import {
  CopyObjectCommand,
  DeleteObjectCommand,
  ListObjectsV2Command,
  PutObjectCommand,
  S3Client,
} from "@aws-sdk/client-s3"
import { randomBytes } from "crypto"
import {
  buildPublicUrl,
  canonicalPublicMediaUrl,
  extractMediaKeysFromHtml,
  extractMediaKeyFromUrl,
} from "@/lib/r2-media-url"

export type { MediaKeyPrefix } from "@/lib/r2-media-url"
export {
  buildPublicUrl,
  getR2PublicBaseUrl,
  resolveR2PublicBaseUrl,
  rewriteLegacyMediaPublicUrl,
  collectBaselineInlineImageKeysFromArticleApiPayload,
  extractMediaKeyFromUrl,
  extractMediaKeysFromHtml,
  extractObjectKeyFromUrl,
} from "@/lib/r2-media-url"

export type R2KeyPrefix = string

export interface UploadImageOptions {
  /** Object key prefix (e.g. `articles/my-slug`, `hospitals/drafts`). */
  prefix?: R2KeyPrefix
  /** Base file name without extension; defaults to random hex. */
  fileBaseName?: string
}

export interface UploadImageResult {
  url: string
  /** Full R2 object key including file extension. */
  objectKey: string
  /** Logical media key (path without extension) — same value stored in `imagePublicId` fields. */
  mediaKey: string
}

let s3Client: S3Client | null = null

function getS3Client(): S3Client {
  if (!s3Client) {
    s3Client = new S3Client({
      region: "auto",
      endpoint: process.env.R2_ENDPOINT,
      credentials: {
        accessKeyId: process.env.R2_ACCESS_KEY_ID ?? "",
        secretAccessKey: process.env.R2_SECRET_ACCESS_KEY ?? "",
      },
    })
  }
  return s3Client
}

function getBucket(): string {
  return process.env.R2_BUCKET ?? ""
}

function mimeToExt(mime: string): string {
  const map: Record<string, string> = {
    "image/jpeg": "jpg",
    "image/jpg": "jpg",
    "image/png": "png",
    "image/gif": "gif",
    "image/webp": "webp",
    "image/svg+xml": "svg",
    "image/avif": "avif",
  }
  return map[mime.toLowerCase()] ?? "jpg"
}

function parseDataUri(dataUri: string): { buffer: Buffer; mime: string } {
  const match = /^data:([^;]+);base64,(.+)$/i.exec(dataUri.trim())
  if (!match) throw new Error("Invalid data URI")
  return {
    mime: match[1],
    buffer: Buffer.from(match[2], "base64"),
  }
}

function generateFileName(): string {
  return randomBytes(8).toString("hex")
}

export function objectKeyToMediaKey(objectKey: string): string {
  const normalized = objectKey.replace(/^\/+/, "")
  const lastSlash = normalized.lastIndexOf("/")
  const filename = lastSlash >= 0 ? normalized.slice(lastSlash + 1) : normalized
  const dir = lastSlash >= 0 ? normalized.slice(0, lastSlash) : ""
  const dotIndex = filename.lastIndexOf(".")
  const nameWithoutExt = dotIndex > 0 ? filename.slice(0, dotIndex) : filename
  return dir ? `${dir}/${nameWithoutExt}` : nameWithoutExt
}

export function mediaKeyToObjectKeyCandidates(mediaKey: string): string {
  return mediaKey.trim().replace(/^\/+/, "")
}

async function listObjectKeysWithPrefix(prefix: string): Promise<string[]> {
  const client = getS3Client()
  const bucket = getBucket()
  const keys: string[] = []
  let continuationToken: string | undefined

  do {
    const res = await client.send(
      new ListObjectsV2Command({
        Bucket: bucket,
        Prefix: prefix,
        ContinuationToken: continuationToken,
      })
    )
    for (const obj of res.Contents ?? []) {
      if (typeof obj.Key === "string" && obj.Key.trim()) keys.push(obj.Key.trim())
    }
    continuationToken = res.IsTruncated ? res.NextContinuationToken : undefined
  } while (continuationToken)

  return keys
}

export async function uploadImage(
  dataUri: string,
  options: UploadImageOptions = {}
): Promise<UploadImageResult> {
  const prefix = (options.prefix ?? "artical").replace(/^\/+|\/+$/g, "")
  const { buffer, mime } = parseDataUri(dataUri)
  const ext = mimeToExt(mime)
  const fileName = (options.fileBaseName ?? "").trim() || generateFileName()
  const safeBase = fileName.replace(/^\/+|\/+$/g, "").split("/").pop() ?? generateFileName()
  const mediaKey = `${prefix}/${safeBase}`
  const objectKey = `${mediaKey}.${ext}`

  await getS3Client().send(
    new PutObjectCommand({
      Bucket: getBucket(),
      Key: objectKey,
      Body: buffer,
      ContentType: mime,
    })
  )

  const url = canonicalPublicMediaUrl(buildPublicUrl(objectKey), mediaKey)
  return { url, objectKey, mediaKey }
}

/** Delete a single R2 object by exact media key or full object key (best effort; does not throw). */
export async function deleteImage(keyOrMediaKey?: string | null) {
  const input = typeof keyOrMediaKey === "string" ? keyOrMediaKey.trim().replace(/^\/+/, "") : ""
  if (!input) return

  try {
    const toDelete: string[] = []

    if (input.includes(".")) {
      // Full object key (includes extension) — delete that object only.
      toDelete.push(input)
    } else {
      // Logical media key — list objects in the parent folder and match the key exactly.
      // Do NOT use the media key itself as an S3 list prefix: keys like
      // `…/content-images/content-images` would also match `…/content-images/content-images-2`.
      const mediaKey = input
      const lastSlash = mediaKey.lastIndexOf("/")
      const listPrefix = lastSlash >= 0 ? `${mediaKey.slice(0, lastSlash)}/` : `${mediaKey}.`
      const keys = await listObjectKeysWithPrefix(listPrefix)
      for (const objectKey of keys) {
        if (objectKeyToMediaKey(objectKey) === mediaKey) {
          toDelete.push(objectKey)
        }
      }
    }

    if (toDelete.length === 0) return

    const client = getS3Client()
    const bucket = getBucket()
    await Promise.allSettled(
      toDelete.map((objectKey) =>
        client.send(new DeleteObjectCommand({ Bucket: bucket, Key: objectKey }))
      )
    )
  } catch (e) {
    if (process.env.NODE_ENV === "development") {
      console.warn("[r2-storage] delete failed for", input, e)
    }
  }
}

export async function listMediaKeysUnderPrefix(prefix: string): Promise<string[]> {
  const normalized = `${prefix.replace(/\/+$/, "")}/`
  const keys = await listObjectKeysWithPrefix(normalized)
  return keys.map((objectKey) => objectKeyToMediaKey(objectKey)).filter(Boolean)
}

function encodeS3CopySource(bucket: string, objectKey: string): string {
  const encodedKey = objectKey
    .split("/")
    .map((segment) => encodeURIComponent(segment))
    .join("/")
  return `${bucket}/${encodedKey}`
}

/**
 * Move every object under `oldPrefix/` to the same relative path under `newPrefix/`.
 * Used when a hospital slug changes so R2 keys stay in sync with `hospitals/{slug}/...`.
 */
export async function relocateObjectsUnderPrefix(
  oldPrefix: string,
  newPrefix: string
): Promise<{ moved: number }> {
  const oldBase = oldPrefix.trim().replace(/^\/+|\/+$/g, "")
  const newBase = newPrefix.trim().replace(/^\/+|\/+$/g, "")
  if (!oldBase || !newBase || oldBase === newBase) return { moved: 0 }

  const objectKeys = await listObjectKeysWithPrefix(`${oldBase}/`)
  if (objectKeys.length === 0) return { moved: 0 }

  const client = getS3Client()
  const bucket = getBucket()
  let moved = 0

  for (const oldKey of objectKeys) {
    if (!oldKey.startsWith(`${oldBase}/`) && oldKey !== oldBase) continue
    const suffix = oldKey.startsWith(`${oldBase}/`) ? oldKey.slice(oldBase.length) : ""
    const newKey = `${newBase}${suffix}`
    if (newKey === oldKey) continue

    await client.send(
      new CopyObjectCommand({
        Bucket: bucket,
        Key: newKey,
        CopySource: encodeS3CopySource(bucket, oldKey),
      })
    )
    await client.send(new DeleteObjectCommand({ Bucket: bucket, Key: oldKey }))
    moved += 1
  }

  return { moved }
}

/** Delete all objects under a prefix except the listed media keys. */
export async function purgePrefixExcept(
  prefix?: string | null,
  keepMediaKeys: Iterable<string> = []
) {
  const target = typeof prefix === "string" ? prefix.trim().replace(/\/+$/, "") : ""
  if (!target) return

  const keep = new Set(
    [...keepMediaKeys]
      .map((key) => (typeof key === "string" ? key.trim() : ""))
      .filter(Boolean)
  )

  try {
    const allKeys = await listMediaKeysUnderPrefix(target)
    const toDelete = allKeys.filter((mediaKey) => !keep.has(mediaKey))
    if (toDelete.length === 0) return
    await Promise.allSettled(toDelete.map((mediaKey) => deleteImage(mediaKey)))
  } catch (e) {
    if (process.env.NODE_ENV === "development") {
      console.warn(
        "[r2-storage] purgePrefixExcept failed for",
        target,
        e instanceof Error ? e.message : e
      )
    }
  }
}

/** No-op when prefix has no objects (R2 has no real folders). */
export async function deletePrefixIfEmpty(prefix?: string | null) {
  const target = typeof prefix === "string" ? prefix.trim().replace(/\/+$/, "") : ""
  if (!target) return
  try {
    const keys = await listObjectKeysWithPrefix(`${target}/`)
    if (keys.length > 0) return
  } catch (e) {
    if (process.env.NODE_ENV === "development") {
      const msg = e instanceof Error ? e.message : String(e)
      console.warn("[r2-storage] deletePrefixIfEmpty check skipped for", target, msg)
    }
  }
}

export function isManagedArticleInlineImageKey(mediaKey?: string | null): boolean {
  const key = typeof mediaKey === "string" ? mediaKey.trim() : ""
  return key.startsWith("articles/")
}
