import {
  collectBaselineInlineImageKeysFromArticleApiPayload,
  deleteImage,
  deletePrefixIfEmpty,
  extractMediaKeyFromUrl,
  purgePrefixExcept,
} from "@/lib/r2-storage"
import { articleMediaBaseForSlug } from "@/lib/article-media"
import {
  collectArticleOldMediaBases,
  normalizeMediaBase,
  rewriteMediaPathsMulti,
  uniqueMediaBases,
} from "@/lib/media-manager/slug-relocate"

/** R2 media keys owned by dashboard entity media managers. */
export const MANAGED_ENTITY_MEDIA_ROOTS = ["articles", "hospitals", "doctors", "procedures"] as const

export function isManagedEntityMediaKey(mediaKey?: string | null): boolean {
  const key = typeof mediaKey === "string" ? mediaKey.trim() : ""
  const root = key.split("/")[0]
  return (MANAGED_ENTITY_MEDIA_ROOTS as readonly string[]).includes(root)
}

/** @deprecated Use `isManagedEntityMediaKey` */
export function isManagedArticleMediaKey(mediaKey?: string | null): boolean {
  return isManagedEntityMediaKey(mediaKey)
}

/** `{entity}/{segment}` root for a media key (e.g. `articles/my-slug`). */
export function entityMediaBaseFromMediaKey(mediaKey: string): string | null {
  const parts = mediaKey.trim().replace(/^\/+/, "").split("/")
  if (!parts[0] || !parts[1]) return null
  if (!(MANAGED_ENTITY_MEDIA_ROOTS as readonly string[]).includes(parts[0])) return null
  return `${parts[0]}/${parts[1]}`
}

function walkManagedMediaKeyStrings(value: unknown, out: Set<string>) {
  if (value === null || value === undefined) return
  if (typeof value === "string") {
    const t = value.trim()
    if (isManagedEntityMediaKey(t)) {
      out.add(t)
      return
    }
    for (const root of MANAGED_ENTITY_MEDIA_ROOTS) {
      const fromUrl = extractMediaKeyFromUrl(t, { prefix: root })
      if (fromUrl) out.add(fromUrl)
    }
    return
  }
  if (Array.isArray(value)) {
    for (const item of value) walkManagedMediaKeyStrings(item, out)
    return
  }
  if (typeof value === "object") {
    for (const [key, raw] of Object.entries(value as Record<string, unknown>)) {
      const lower = key.toLowerCase()
      if (typeof raw === "string") {
        const t = raw.trim()
        if (lower.includes("publicid") && isManagedEntityMediaKey(t)) {
          out.add(t)
          continue
        }
        walkManagedMediaKeyStrings(raw, out)
        continue
      }
      walkManagedMediaKeyStrings(raw, out)
    }
  }
}

/** Collect all managed entity media keys referenced anywhere in a save payload. */
export function collectManagedMediaKeysFromPayload(payload: unknown): Set<string> {
  const out = new Set<string>()
  walkManagedMediaKeyStrings(payload, out)
  return out
}

/** Entity media roots in `keys` that differ from the current media base. */
export function collectStaleMediaBasesFromKeys(
  keys: Iterable<string>,
  currentMediaBase: string
): string[] {
  const current = normalizeMediaBase(currentMediaBase)
  const bases: string[] = []
  const seen = new Set<string>()
  for (const raw of keys) {
    const base = entityMediaBaseFromMediaKey(typeof raw === "string" ? raw : "")
    if (!base || base === current || seen.has(base)) continue
    seen.add(base)
    bases.push(base)
  }
  return bases
}

/**
 * Rewrite stale entity media paths in a save payload to `currentMediaBase`.
 * Use when the namespace (slug/name) is **unchanged** but the client may still
 * hold URLs from a previous namespace after an earlier save in the same session.
 */
export function normalizeSavePayloadMediaToCurrentBase(
  payload: unknown,
  input: {
    persistedOldBases: Array<string | null | undefined>
    currentMediaBase: string
  }
): unknown {
  const current = normalizeMediaBase(input.currentMediaBase)
  const payloadKeys = collectManagedMediaKeysFromPayload(payload)
  const oldBases = uniqueMediaBases([
    ...input.persistedOldBases,
    ...collectStaleMediaBasesFromKeys(payloadKeys, current),
  ])
  return rewriteMediaPathsMulti(payload, oldBases, current)
}

/** Trimmed non-empty media keys / public ids as stored in DB (R2 logical keys). */
export function normalizeMediaKeySet(keys: Iterable<string>): Set<string> {
  const out = new Set<string>()
  for (const k of keys) {
    const t = typeof k === "string" ? k.trim() : ""
    if (t) out.add(t)
  }
  return out
}

/** Keys present in `previous` but not in `next` (exact string equality on normalized keys). */
export function diffRemovedMediaKeys(previous: Iterable<string>, next: Iterable<string>): string[] {
  const prev = normalizeMediaKeySet(previous)
  const nxt = normalizeMediaKeySet(next)
  return [...prev].filter((k) => !nxt.has(k))
}

/** True if `key` is exactly `base` or a descendant object key under `base/`. */
export function mediaKeyUnderAnyBase(key: string, bases: Array<string | null | undefined>): boolean {
  const k = key.trim()
  if (!k) return false
  for (const base of uniqueMediaBases(bases)) {
    if (k === base || k.startsWith(`${base}/`)) return true
  }
  return false
}

/**
 * Deletes R2 objects for media keys that are no longer referenced, while keys still
 * reflect the **pre-relocate** namespace. Call this **before** `relocateMediaBases` when
 * the slug (or folder) changes so removed blobs are not copied into the new prefix.
 *
 * Only keys under `namespacePrefixes` are deleted (safety guard for unrelated keys).
 *
 * Important: `nextMediaKeys` must use the **same** slug namespace as `previousMediaKeys`
 * (typically the old slug). Do not pass payload keys already rewritten to the new slug.
 */
export async function deleteRemovedMediaKeysBeforeNamespaceMove(input: {
  previousMediaKeys: Iterable<string>
  nextMediaKeys: Iterable<string>
  /** e.g. `articles/old-slug`, `hospitals/old-slug` */
  namespacePrefixes: Array<string | null | undefined>
}): Promise<{ removedKeys: string[] }> {
  const prefixes = uniqueMediaBases(input.namespacePrefixes)
  if (prefixes.length === 0) return { removedKeys: [] }

  const removed = diffRemovedMediaKeys(input.previousMediaKeys, input.nextMediaKeys)
  const deleted: string[] = []
  for (const key of removed) {
    if (!mediaKeyUnderAnyBase(key, prefixes)) continue
    await deleteImage(key)
    deleted.push(key)
  }
  return { removedKeys: deleted }
}

/**
 * After save: drop orphan objects under the entity’s **new** media root only.
 * Do not use the old slug prefix for keep/deny decisions after relocation.
 */
export async function purgeEntityMediaRootExcept(
  mediaRootPrefix: string | null | undefined,
  keepMediaKeys: Iterable<string>
): Promise<void> {
  const root = typeof mediaRootPrefix === "string" ? mediaRootPrefix.trim().replace(/\/+$/, "") : ""
  if (!root) return
  const keep = normalizeMediaKeySet(keepMediaKeys)
  await purgePrefixExcept(root, keep)
  await deletePrefixIfEmpty(root)
}

function walkArticlePublicIdStrings(value: unknown, out: Set<string>) {
  if (value === null || value === undefined) return
  if (typeof value === "string") {
    const t = value.trim()
    if (t.startsWith("articles/")) out.add(t)
    return
  }
  if (Array.isArray(value)) {
    for (const item of value) walkArticlePublicIdStrings(item, out)
    return
  }
  if (typeof value === "object") {
    for (const [key, raw] of Object.entries(value as Record<string, unknown>)) {
      const lower = key.toLowerCase()
      if (typeof raw === "string") {
        const t = raw.trim()
        if (lower.includes("publicid") && t.startsWith("articles/")) out.add(t)
        continue
      }
      walkArticlePublicIdStrings(raw, out)
    }
  }
}

/**
 * Media keys referenced by the article save payload (cover + inline HTML + nested `*PublicId` under `articles/`).
 */
export function collectArticleMediaKeysFromSavePayload(payload: unknown): Set<string> {
  const out = new Set<string>()
  if (!payload || typeof payload !== "object") return out
  const body = payload as Record<string, unknown>

  for (const k of collectBaselineInlineImageKeysFromArticleApiPayload(payload)) {
    out.add(k)
  }

  const img = typeof body.imagePublicId === "string" ? body.imagePublicId.trim() : ""
  if (img.startsWith("articles/")) out.add(img)

  const imageUrl = typeof body.image === "string" ? body.image.trim() : ""
  const fromCover = extractMediaKeyFromUrl(imageUrl, { prefix: "articles" })
  if (fromCover) out.add(fromCover)

  walkArticlePublicIdStrings(body.translations, out)
  walkArticlePublicIdStrings(body.overview, out)
  walkArticlePublicIdStrings(body.faq, out)
  walkArticlePublicIdStrings(body.conversionCard, out)
  walkArticlePublicIdStrings(body.trustCard, out)

  return out
}

/** `articles/{slug-segment}` root for a media key. */
export function articleMediaBaseFromMediaKey(mediaKey: string): string | null {
  const base = entityMediaBaseFromMediaKey(mediaKey)
  return base?.startsWith("articles/") ? base : null
}

/** Article media roots in `keys` that differ from the current slug folder. */
export function collectStaleArticleMediaBasesFromKeys(
  keys: Iterable<string>,
  currentMediaBase: string
): string[] {
  return collectStaleMediaBasesFromKeys(keys, currentMediaBase)
}

/**
 * Rewrite stale `articles/{old-slug}/…` paths in an incoming save payload to the
 * current slug media root. Only when slug is **unchanged** — see PUT route branching.
 */
export function normalizeArticleSavePayloadMediaToCurrentSlug(
  payload: unknown,
  input: {
    persistedSlug: string
    persistedContentImageFolder?: string | null
    targetSlug: string
  }
): unknown {
  return normalizeSavePayloadMediaToCurrentBase(payload, {
    persistedOldBases: [
      ...collectArticleOldMediaBases({
        slug: input.persistedSlug,
        contentImageFolder: input.persistedContentImageFolder,
      }),
      articleMediaBaseForSlug(input.persistedSlug),
    ],
    currentMediaBase: articleMediaBaseForSlug(input.targetSlug),
  })
}

/** Media keys referenced by a persisted article row (before a save). */
export function collectArticleMediaKeysFromPersistedArticle(input: {
  image?: string | null
  imagePublicId?: string | null
  content?: string | null
  translations?: unknown
  overview?: unknown
  faq?: unknown
  conversionCard?: unknown
  trustCard?: unknown
}): Set<string> {
  return collectArticleMediaKeysFromSavePayload({
    image: input.image ?? "",
    imagePublicId: input.imagePublicId ?? "",
    content: input.content ?? "",
    translations: input.translations,
    overview: input.overview,
    faq: input.faq,
    conversionCard: input.conversionCard,
    trustCard: input.trustCard,
  })
}

/** Delete R2 objects for article media keys dropped from the saved payload. */
export async function deleteRemovedArticleMediaKeys(
  previousMediaKeys: Iterable<string>,
  nextMediaKeys: Iterable<string>,
  currentMediaBase?: string
): Promise<{ deletedKeys: string[] }> {
  const removed = diffRemovedMediaKeys(previousMediaKeys, nextMediaKeys).filter(isManagedEntityMediaKey)
  const deleted: string[] = []
  for (const key of removed) {
    if (currentMediaBase && !mediaKeyUnderAnyBase(key, [currentMediaBase])) continue
    await deleteImage(key)
    deleted.push(key)
  }
  return { deletedKeys: deleted }
}

/** Map persisted media keys from old entity folders onto the new media root. */
export function rewriteMediaKeysToNewBase(
  keys: Iterable<string>,
  oldBases: Array<string | null | undefined>,
  newBase: string
): Set<string> {
  const newNorm = normalizeMediaBase(newBase)
  const out = new Set<string>()
  for (const raw of keys) {
    const key = typeof raw === "string" ? raw.trim() : ""
    if (!key) continue
    let rewritten = key
    for (const oldBase of uniqueMediaBases(oldBases)) {
      if (oldBase === newNorm) continue
      if (key === oldBase || key.startsWith(`${oldBase}/`)) {
        rewritten = `${newNorm}${key.slice(oldBase.length)}`
        break
      }
    }
    out.add(rewritten)
  }
  return out
}

/** @deprecated Use `rewriteMediaKeysToNewBase` */
export const rewriteArticleMediaKeysToNewBase = rewriteMediaKeysToNewBase

/**
 * After a namespace change (slug/name), delete media keys dropped from the payload
 * that now live under the new media root (post-relocate).
 */
export async function deleteRemovedMediaKeysAfterNamespaceChange(input: {
  previousMediaKeys: Iterable<string>
  nextMediaKeys: Iterable<string>
  oldBases: Array<string | null | undefined>
  newMediaBase: string
}): Promise<{ deletedKeys: string[] }> {
  const previousAtNewNamespace = rewriteMediaKeysToNewBase(
    input.previousMediaKeys,
    input.oldBases,
    input.newMediaBase
  )
  const removedAtNewNamespace = diffRemovedMediaKeys(previousAtNewNamespace, input.nextMediaKeys).filter(
    (key) => mediaKeyUnderAnyBase(key, [input.newMediaBase]) && isManagedEntityMediaKey(key)
  )
  const deleted: string[] = []
  for (const key of removedAtNewNamespace) {
    await deleteImage(key)
    deleted.push(key)
  }
  return { deletedKeys: deleted }
}

/** @deprecated Use `deleteRemovedMediaKeysAfterNamespaceChange` */
export const deleteRemovedArticleMediaKeysAfterSlugChange = deleteRemovedMediaKeysAfterNamespaceChange

export interface ReconcileEntityMediaAfterSaveInput {
  namespaceChanged: boolean
  oldBases: Array<string | null | undefined>
  newMediaBase: string
  previousMediaKeys: Iterable<string>
  savedPayload: unknown
  collectNextMediaKeys?: (payload: unknown) => Set<string>
}

/** Post-save R2 cleanup for any entity with slug/name-based media folders. */
export async function reconcileEntityMediaAfterSave(
  input: ReconcileEntityMediaAfterSaveInput
): Promise<void> {
  const newMediaBase = normalizeMediaBase(input.newMediaBase)
  const nextMediaKeys = input.collectNextMediaKeys
    ? input.collectNextMediaKeys(input.savedPayload)
    : collectManagedMediaKeysFromPayload(input.savedPayload)

  if (input.namespaceChanged) {
    await deleteRemovedMediaKeysAfterNamespaceChange({
      previousMediaKeys: input.previousMediaKeys,
      nextMediaKeys,
      oldBases: input.oldBases,
      newMediaBase,
    })
  } else {
    await deleteRemovedArticleMediaKeys(input.previousMediaKeys, nextMediaKeys, newMediaBase)
  }

  await purgeEntityMediaRootExcept(newMediaBase, nextMediaKeys)

  if (input.namespaceChanged) {
    for (const oldBase of uniqueMediaBases(input.oldBases)) {
      if (normalizeMediaBase(oldBase) === newMediaBase) continue
      await deletePrefixIfEmpty(oldBase)
    }
  }
}

export interface ReconcileArticleMediaAfterSaveInput {
  slugChanged: boolean
  existingSlug: string
  existingContentImageFolder?: string | null
  newSlug: string
  previousMediaKeys: Iterable<string>
  savedPayload: unknown
}

/** Post-save R2 cleanup for articles. */
export async function reconcileArticleMediaAfterSave(
  input: ReconcileArticleMediaAfterSaveInput
): Promise<void> {
  const newMediaBase = articleMediaBaseForSlug(input.newSlug)
  const oldBases = collectArticleOldMediaBases({
    slug: input.existingSlug,
    contentImageFolder: input.existingContentImageFolder,
  })

  await reconcileEntityMediaAfterSave({
    namespaceChanged: input.slugChanged,
    oldBases,
    newMediaBase,
    previousMediaKeys: input.previousMediaKeys,
    savedPayload: input.savedPayload,
    collectNextMediaKeys: collectArticleMediaKeysFromSavePayload,
  })
}

