/** Prépare le HTML d'un mail pour affichage dans une iframe sandboxée. */

import { stripHiddenEmailHtml } from "@/lib/strip-hidden-email-html"
import { stripExecutableEmailHtml } from "@/lib/strip-executable-email-html"
import { preprocessEmailHtmlForTheme } from "@/lib/email-preview-dark-styles"
import { resolveCidReference } from "@/lib/mail-cid"

const REMOTE_URL = /^https?:\/\//i
const PROTOCOL_RELATIVE = /^\/\//

export type ParsedEmailHtml = {
  headMarkup: string
  bodyHtml: string
  /** Uniquement si le mail définit explicitement <base href>. */
  documentBaseHref?: string
  /** Base pour résoudre les URLs relatives dans le HTML. */
  resolveBaseHref?: string
}

function escapeHtmlAttr(value: string): string {
  return value
    .replace(/&/g, "&amp;")
    .replace(/"/g, "&quot;")
    .replace(/</g, "&lt;")
}

export { escapeHtmlAttr }

function resolveUrl(url: string, baseHref?: string): string {
  const trimmed = url.trim()
  if (!trimmed || trimmed.startsWith("data:") || trimmed.startsWith("cid:")) {
    return trimmed
  }
  if (trimmed.startsWith("mailto:") || trimmed.startsWith("#")) return trimmed
  if (REMOTE_URL.test(trimmed)) {
    return trimmed.startsWith("http://")
      ? `https://${trimmed.slice("http://".length)}`
      : trimmed
  }
  if (PROTOCOL_RELATIVE.test(trimmed)) {
    return `https:${trimmed}`
  }
  if (!baseHref) return trimmed
  try {
    const resolved = new URL(trimmed, baseHref).href
    return resolved.startsWith("http://")
      ? `https://${resolved.slice("http://".length)}`
      : resolved
  } catch {
    return trimmed
  }
}

function isStylesheetLink(el: Element): boolean {
  if (el.tagName.toLowerCase() !== "link") return false
  const rel = (el.getAttribute("rel") ?? "").toLowerCase()
  const type = (el.getAttribute("type") ?? "").toLowerCase()
  return rel.includes("stylesheet") || type === "text/css"
}

const LAZY_IMG_SRC_ATTRS = [
  "data-src",
  "data-original",
  "data-lazy-src",
  "data-url",
  "data-href",
  "data-image",
  "originalsrc",
] as const

function firstLazyImageSrc(img: Element): string | null {
  for (const name of LAZY_IMG_SRC_ATTRS) {
    const value = img.getAttribute(name)?.trim()
    if (value) return value
  }
  return null
}

/** Known 1×1 / spacer filenames — not arbitrary URL substrings. */
function isPlaceholderImageSrc(src: string): boolean {
  const t = src.trim().toLowerCase()
  if (!t || t === "about:blank" || t === "#") return true
  if (t.startsWith("data:")) return false
  try {
    const path = new URL(t, "https://example.invalid/").pathname
    const file = path.split("/").pop() ?? ""
    return /^(?:spacer|blank|pixel|1x1|clear|dot|transparent|spacer\.gif)\.(?:gif|png|jpe?g|svg|webp)$/i.test(
      file
    )
  } catch {
    return /(?:^|\/)(?:spacer|blank|pixel|1x1)\.(?:gif|png|jpe?g)/i.test(t)
  }
}

function collectHeadResourceMarkup(
  doc: Document,
  resolveBaseHref?: string
): string {
  const parts: string[] = []
  const nodes = [
    ...doc.head.querySelectorAll("style, link[rel]"),
    ...doc.body.querySelectorAll("style, link[rel]"),
  ]

  for (const node of nodes) {
    const tag = node.tagName.toLowerCase()
    if (tag === "style") {
      parts.push(node.outerHTML)
      node.remove()
      continue
    }
    if (isStylesheetLink(node)) {
      const href = node.getAttribute("href")
      if (href && resolveBaseHref) {
        node.setAttribute("href", resolveUrl(href, resolveBaseHref))
      }
      parts.push(node.outerHTML)
      node.remove()
    }
  }

  return parts.join("\n")
}

export function inferEmailResolveBaseHref(html: string): string | undefined {
  const baseMatch = html.match(/<base[^>]+href=["']([^"']+)["']/i)
  if (baseMatch?.[1]) {
    try {
      return new URL(baseMatch[1]).href
    } catch {
      return baseMatch[1]
    }
  }

  const stylesheetMatch = html.match(
    /<link[^>]+rel=["'][^"']*stylesheet[^"']*["'][^>]+href=["']([^"']+)["']/i
  ) ?? html.match(
    /<link[^>]+href=["']([^"']+)["'][^>]+rel=["'][^"']*stylesheet/i
  )
  if (stylesheetMatch?.[1]) {
    try {
      const u = new URL(stylesheetMatch[1], "https://example.com/")
      if (stylesheetMatch[1].startsWith("http") || stylesheetMatch[1].startsWith("//")) {
        return `${u.origin}${u.pathname.replace(/\/[^/]*$/, "/")}`
      }
    } catch {
      /* ignore */
    }
  }

  const absoluteMatch = html.match(
    /(?:src|href)=["'](https?:\/\/[^/"']+)[^"']*["']/i
  )
  if (absoluteMatch?.[1]) {
    try {
      const u = new URL(absoluteMatch[1])
      return `${u.origin}/`
    } catch {
      /* ignore */
    }
  }

  return undefined
}

/** Extrait styles/CSS + body d'un document HTML complet ou fragment. */
export function parseEmailHtmlForIframe(
  html: string,
  resolveBaseHref?: string
): ParsedEmailHtml {
  if (!html.trim()) {
    return { headMarkup: "", bodyHtml: "" }
  }

  if (typeof DOMParser === "undefined") {
    return { headMarkup: "", bodyHtml: html }
  }

  try {
    const doc = new DOMParser().parseFromString(html, "text/html")
    const explicitBase =
      doc.querySelector("base[href]")?.getAttribute("href") ?? undefined
  const documentBaseHref = explicitBase
      ? resolveUrl(explicitBase, undefined)
      : undefined

    doc.querySelector("base[href]")?.remove()

    const headMarkup = collectHeadResourceMarkup(doc, resolveBaseHref)

    let bodyHtml = doc.body?.innerHTML?.trim() ?? ""
    if (!bodyHtml) bodyHtml = html.trim()

    return {
      headMarkup,
      bodyHtml,
      documentBaseHref,
      resolveBaseHref,
    }
  } catch {
    return { headMarkup: "", bodyHtml: html }
  }
}

function rewriteUrlList(value: string, baseHref?: string): string {
  return value
    .split(",")
    .map((part) => {
      const trimmed = part.trim()
      const space = trimmed.indexOf(" ")
      const url = space === -1 ? trimmed : trimmed.slice(0, space)
      const descriptor = space === -1 ? "" : trimmed.slice(space + 1)
      const resolved = resolveUrl(url, baseHref)
      return descriptor ? `${resolved} ${descriptor}` : resolved
    })
    .join(", ")
}

function rewriteStyleRemoteUrls(style: string, baseHref?: string): string {
  return style.replace(
    /url\s*\(\s*['"]?([^'")]+)['"]?\s*\)/gi,
    (_match, url: string) => {
      const trimmed = url.trim()
      if (
        trimmed.startsWith("data:") ||
        trimmed.startsWith("#") ||
        trimmed.startsWith("cid:")
      ) {
        return `url(${trimmed})`
      }
      return `url(${resolveUrl(trimmed, baseHref)})`
    }
  )
}

function rewriteHeadMarkupUrls(headMarkup: string, baseHref?: string): string {
  if (!headMarkup.trim() || !baseHref) return headMarkup
  return headMarkup
    .replace(
      /(<link[^>]+href=)(["'])([^"']+)\2/gi,
      (_m, prefix: string, quote: string, href: string) =>
        `${prefix}${quote}${resolveUrl(href, baseHref)}${quote}`
    )
    .replace(
      /(<style[^>]*>)([\s\S]*?)(<\/style>)/gi,
      (_m, open: string, css: string, close: string) =>
        `${open}${rewriteStyleRemoteUrls(css, baseHref)}${close}`
    )
}

const REMOTE_CSS_URL =
  /url\s*\(\s*['"]?(?:https?:\/\/|\/\/|[^'"data:#][^'")]*)/i

function neutralizeRemoteUrlsInCss(css: string): string {
  return css.replace(
    /url\s*\(\s*['"]?([^'")]+)['"]?\s*\)/gi,
    (_match, url: string) => {
      const trimmed = url.trim()
      if (
        trimmed.startsWith("data:") ||
        trimmed.startsWith("#") ||
        trimmed.startsWith("cid:")
      ) {
        return `url(${trimmed})`
      }
      if (REMOTE_URL.test(trimmed) || PROTOCOL_RELATIVE.test(trimmed)) {
        return "url(about:blank)"
      }
      return `url(${trimmed})`
    }
  )
}

/** Keep inline <style>; drop external stylesheets when remote content is blocked. */
export function filterHeadMarkupForBlockedRemote(headMarkup: string): string {
  if (!headMarkup.trim() || typeof DOMParser === "undefined") {
    return headMarkup.replace(/<link\b[^>]*>/gi, "")
  }
  try {
    const doc = new DOMParser().parseFromString(
      `<div id="head-root">${headMarkup}</div>`,
      "text/html"
    )
    const root = doc.getElementById("head-root")
    if (!root) return headMarkup

    for (const link of root.querySelectorAll("link")) {
      if (isStylesheetLink(link)) link.remove()
    }
    for (const style of root.querySelectorAll("style")) {
      const css = style.textContent ?? ""
      if (css) style.textContent = neutralizeRemoteUrlsInCss(css)
    }
    return root.innerHTML.trim()
  } catch {
    return headMarkup.replace(/<link\b[^>]*>/gi, "")
  }
}

function clearRemoteImageAttrs(img: Element): void {
  for (const name of [
    "src",
    "srcset",
    "sizes",
    "background",
    ...LAZY_IMG_SRC_ATTRS,
  ] as const) {
    img.removeAttribute(name)
  }
}

/** Newsletter HTML: keep layout/text; block remote images and external CSS. */
export function stripRemoteResourcesForPreview(html: string): string {
  if (!html.trim() || typeof DOMParser === "undefined") return html

  try {
    const doc = new DOMParser().parseFromString(html, "text/html")

    for (const link of doc.querySelectorAll("link")) {
      if (isStylesheetLink(link)) link.remove()
    }

    for (const el of doc.querySelectorAll("[style]")) {
      const style = el.getAttribute("style")
      if (style && REMOTE_CSS_URL.test(style)) {
        el.setAttribute("style", neutralizeRemoteUrlsInCss(style))
      }
    }

    for (const el of doc.querySelectorAll("[background]")) {
      const bg = el.getAttribute("background")?.trim() ?? ""
      if (bg && isRemoteOrRelativeUrl(bg)) el.removeAttribute("background")
    }

    for (const img of doc.querySelectorAll("img")) {
      const src = img.getAttribute("src") ?? ""
      const lazy = firstLazyImageSrc(img)
      const remote =
        (src.trim() && isRemoteOrRelativeUrl(src) && !src.startsWith("cid:")) ||
        Boolean(lazy && isRemoteOrRelativeUrl(lazy))

      if (!remote) continue

      const alt = img.getAttribute("alt")?.trim()
      clearRemoteImageAttrs(img)
      img.setAttribute(
        "style",
        `${img.getAttribute("style") ?? ""};max-width:100%;height:auto;object-fit:contain;background:#e8eaed;border-radius:4px;`.replace(
          /^;/,
          ""
        )
      )

      if (alt) {
        const caption = doc.createElement("p")
        caption.setAttribute(
          "style",
          "margin:4px 0 12px;font:13px/1.4 sans-serif;color:#5f6368"
        )
        caption.textContent = alt
        img.insertAdjacentElement("afterend", caption)
      }
    }

    return doc.body.innerHTML
  } catch {
    return html
  }
}

export function htmlHasMeaningfulVisibleText(html: string, minChars = 24): boolean {
  if (!html.trim() || typeof DOMParser === "undefined") {
    return html.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim().length >= minChars
  }
  try {
    const doc = new DOMParser().parseFromString(html, "text/html")
    const text = (doc.body?.textContent ?? "").replace(/\s+/g, " ").trim()
    return text.length >= minChars
  } catch {
    return false
  }
}

export function plainTextFallbackHtml(text: string): string {
  const trimmed = text.trim()
  if (!trimmed) return ""
  const escaped = trimmed
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
  return (
    `<div class="ultimail-plain-fallback" style="margin:0 0 16px;padding:12px 14px;` +
    `border-radius:8px;background:var(--muted,#f1f3f4);color:inherit">` +
    `<pre style="white-space:pre-wrap;font:14px/1.5 inherit;margin:0">` +
    `${escaped}</pre></div>`
  )
}

function isRemoteOrRelativeUrl(url: string): boolean {
  const t = url.trim()
  return (
    REMOTE_URL.test(t) ||
    PROTOCOL_RELATIVE.test(t) ||
    (!t.startsWith("data:") &&
      !t.startsWith("cid:") &&
      !t.startsWith("mailto:") &&
      !t.startsWith("#") &&
      !t.startsWith("javascript:"))
  )
}

/** Active images lazy-load et résout les URLs relatives / http. */
export function activateRemoteResourcesInHtml(
  html: string,
  options: { baseHref?: string } = {}
): string {
  const { baseHref } = options
  if (!html.trim() || typeof DOMParser === "undefined") return html

  try {
    const doc = new DOMParser().parseFromString(html, "text/html")

    for (const img of doc.querySelectorAll("img")) {
      const lazySrc = firstLazyImageSrc(img)
      const src = img.getAttribute("src") ?? ""
      const srcIsUseful =
        !!src.trim() &&
        !isPlaceholderImageSrc(src) &&
        (src.startsWith("data:") ||
          src.startsWith("cid:") ||
          isRemoteOrRelativeUrl(src))

      if (lazySrc && isRemoteOrRelativeUrl(lazySrc) && !srcIsUseful) {
        img.setAttribute("src", resolveUrl(lazySrc, baseHref))
      } else if (src.trim() && (src.startsWith("data:") || src.startsWith("cid:"))) {
        /* keep inline / embedded sources */
      } else if (src && isRemoteOrRelativeUrl(src)) {
        img.setAttribute("src", resolveUrl(src, baseHref))
      } else if (lazySrc && isRemoteOrRelativeUrl(lazySrc)) {
        img.setAttribute("src", resolveUrl(lazySrc, baseHref))
      }

      const srcset = img.getAttribute("srcset")
      if (srcset) img.setAttribute("srcset", rewriteUrlList(srcset, baseHref))

      const dataSrcset = img.getAttribute("data-srcset")
      if (dataSrcset && !img.getAttribute("srcset")) {
        img.setAttribute("srcset", rewriteUrlList(dataSrcset, baseHref))
      }

      img.setAttribute("referrerpolicy", "no-referrer")
    }

    for (const el of doc.querySelectorAll("[background]")) {
      const bg = el.getAttribute("background")
      if (!bg || !isRemoteOrRelativeUrl(bg)) continue
      const resolved = resolveUrl(bg, baseHref)
      el.removeAttribute("background")
      const existing = el.getAttribute("style")?.trim() ?? ""
      const bgRule = `background-image:url(${resolved})`
      const nextStyle = existing
        ? existing.endsWith(";")
          ? `${existing}${bgRule}`
          : `${existing};${bgRule}`
        : bgRule
      el.setAttribute("style", nextStyle)
    }

    for (const el of doc.querySelectorAll("[style]")) {
      const style = el.getAttribute("style")
      if (style?.includes("url(")) {
        el.setAttribute("style", rewriteStyleRemoteUrls(style, baseHref))
      }
    }

    for (const source of doc.querySelectorAll("source[src], source[srcset]")) {
      const src = source.getAttribute("src")
      if (src && isRemoteOrRelativeUrl(src)) {
        source.setAttribute("src", resolveUrl(src, baseHref))
      }
      const srcset = source.getAttribute("srcset")
      if (srcset) source.setAttribute("srcset", rewriteUrlList(srcset, baseHref))
    }

    return doc.body.innerHTML
  } catch {
    return html
  }
}

const EMAIL_PREVIEW_GEIST_FONT_LINKS =
  '<link rel="preconnect" href="https://fonts.googleapis.com">' +
  '<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>' +
  '<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&display=swap">'

/** Full HTML document for iframe srcDoc (scripts stripped; no live DOM injection). */
export function buildEmailPreviewSrcdoc(
  parsed: ParsedEmailHtml,
  options: {
    csp: string
    wrapperCss: string
    plainTextFallback?: string
    /** CSS injecté en fin de body (après styles expéditeur inline). */
    bodyTailCss?: string
    /** Charge Geist (CSP doit autoriser fonts.googleapis.com). */
    loadAppFont?: boolean
  }
): string {
  const baseHref = parsed.documentBaseHref ?? parsed.resolveBaseHref
  // headMarkup is CSS (<style> blocks) — only strip script-like tags via regex
  const headMarkup = (parsed.headMarkup ?? "")
    .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "")
    .replace(/<script\b[^>]*\/>/gi, "")
  let bodyHtml = stripExecutableEmailHtml(parsed.bodyHtml)

  if (!bodyHtml.trim() && options.plainTextFallback?.trim()) {
    bodyHtml = plainTextFallbackHtml(options.plainTextFallback)
  }
  if (!bodyHtml.trim()) {
    bodyHtml =
      '<p style="color:#3c4043;font:14px ui-sans-serif,system-ui,sans-serif;margin:0">Ce message n\'a pas de contenu affichable.</p>'
  }

  const baseTag = baseHref
    ? `<base href="${escapeHtmlAttr(baseHref)}" target="_blank">`
    : ""

  const bodyTailStyle = options.bodyTailCss
    ? `<style data-ultimail-tail="true">${options.bodyTailCss}</style>`
    : ""

  const fontLinks = options.loadAppFont ? EMAIL_PREVIEW_GEIST_FONT_LINKS : ""

  return (
    "<!DOCTYPE html><html><head>" +
    '<meta charset="utf-8">' +
    `<meta http-equiv="Content-Security-Policy" content="${escapeHtmlAttr(options.csp)}">` +
    baseTag +
    fontLinks +
    `<style data-ultimail-wrapper="true">${options.wrapperCss}</style>` +
    headMarkup +
    "</head><body>" +
    `<div data-ultimail-measure-root>${bodyHtml}</div>` +
    bodyTailStyle +
    "</body></html>"
  )
}

/** Replace cid: references with blob URLs, or a data placeholder until blobs load. */
export function rewriteCidUrlsInHtml(
  html: string,
  cidUrlMap: Record<string, string> | undefined
): string {
  if (!html.trim() || typeof DOMParser === "undefined") return html
  if (!html.includes("cid:")) return html

  const resolveCid = (value: string): string => resolveCidReference(cidUrlMap, value)

  const CID_ATTRS = [
    "src",
    "data-src",
    "data-original",
    "data-lazy-src",
    "background",
    "poster",
  ] as const

  try {
    const doc = new DOMParser().parseFromString(html, "text/html")
    for (const img of doc.querySelectorAll("img, video, source, table, td, th, div, span")) {
      for (const attr of CID_ATTRS) {
        const value = img.getAttribute(attr)
        if (value?.toLowerCase().includes("cid:")) {
          img.setAttribute(
            attr,
            value.replace(/cid:[^\s'")]+/gi, (match) => resolveCid(match))
          )
        }
      }
      const srcset = img.getAttribute("srcset")
      if (srcset?.includes("cid:")) {
        img.setAttribute(
          "srcset",
          srcset.replace(/cid:[^\s,]+/gi, (match) => resolveCid(match))
        )
      }
    }
    for (const el of doc.querySelectorAll("[style]")) {
      const style = el.getAttribute("style")
      if (style?.includes("cid:")) {
        el.setAttribute(
          "style",
          style.replace(/cid:[^\s'")]+/gi, (match) => resolveCid(match))
        )
      }
    }
    return doc.body.innerHTML
  } catch {
    return html
  }
}

function maybePrependPlainTextFallback(
  bodyHtml: string,
  plainTextFallback?: string
): string {
  const fallback = plainTextFallback?.trim()
  if (!fallback || htmlHasMeaningfulVisibleText(bodyHtml)) {
    return bodyHtml
  }
  return plainTextFallbackHtml(fallback) + (bodyHtml || "")
}

export function prepareEmailHtmlForIframe(
  html: string,
  options: {
    blockRemoteContent: boolean
    isDark: boolean
    senderEmail?: string
    cidUrlMap?: Record<string, string>
    plainTextFallback?: string
  }
): ParsedEmailHtml {
  if (!html.trim()) {
    const fallback = options.plainTextFallback?.trim()
    if (fallback) {
      return { headMarkup: "", bodyHtml: plainTextFallbackHtml(fallback) }
    }
    return { headMarkup: "", bodyHtml: "" }
  }

  const resolveBaseHref =
    inferEmailResolveBaseHref(html) ??
    (options.senderEmail
      ? `https://${options.senderEmail.split("@")[1]?.trim().toLowerCase()}/`
      : undefined)

  if (options.blockRemoteContent) {
    const parsed = parseEmailHtmlForIframe(html, resolveBaseHref)
    let bodyHtml = stripHiddenEmailHtml(parsed.bodyHtml || html)
    bodyHtml = stripRemoteResourcesForPreview(bodyHtml)
    bodyHtml = rewriteCidUrlsInHtml(
      preprocessEmailHtmlForTheme(bodyHtml, options.isDark),
      options.cidUrlMap
    )
    const headMarkup = rewriteCidUrlsInHtml(
      filterHeadMarkupForBlockedRemote(parsed.headMarkup),
      options.cidUrlMap
    )
    bodyHtml = maybePrependPlainTextFallback(bodyHtml, options.plainTextFallback)
    return {
      headMarkup,
      bodyHtml,
      resolveBaseHref,
    }
  }

  const parsed = parseEmailHtmlForIframe(html, resolveBaseHref)
  let bodyHtml = stripHiddenEmailHtml(parsed.bodyHtml)
  bodyHtml = activateRemoteResourcesInHtml(bodyHtml, {
    baseHref: resolveBaseHref,
  })
  bodyHtml = rewriteCidUrlsInHtml(bodyHtml, options.cidUrlMap)
  bodyHtml = maybePrependPlainTextFallback(bodyHtml, options.plainTextFallback)

  const headMarkup = rewriteCidUrlsInHtml(
    rewriteHeadMarkupUrls(parsed.headMarkup, resolveBaseHref),
    options.cidUrlMap
  )

  return {
    headMarkup,
    bodyHtml,
    documentBaseHref: parsed.documentBaseHref,
    resolveBaseHref,
  }
}
