/**
 * Finding and rewriting image references inside EDM html.
 *
 * ── Why this does not use a DOM parser ───────────────────────────────────────
 *
 * The obvious implementation is parse → mutate → serialise. It is wrong here.
 * Email html is full of constructs a spec-compliant parser will not round-trip:
 *
 *   <!--[if gte mso 9]> … <![endif]-->   Outlook conditional comments — a
 *                                        parser sees a comment and a serialiser
 *                                        may normalise or drop the payload
 *   <v:rect>, <v:fill>, <o:…>            VML / Office namespaced elements
 *   deliberately unclosed <td> hacks     kept on purpose for old clients
 *
 * Reserialising rewrites bytes we were never asked to touch, and the damage
 * shows up only in the one client the hack existed for. So instead we locate
 * each URL's exact offset span and splice, leaving every other byte identical.
 *
 * ── Why the net is this wide ─────────────────────────────────────────────────
 *
 * The console's existing path check only inspects <img src> and <a href>. Email
 * hides images in at least four more places, and each one missed is an image
 * that preflight calls clean and that renders as a hole in the inbox.
 */

export type ImageRefKind =
  | "img-src"
  | "img-srcset"
  | "background-attr"
  | "css-url"
  | "vml-src";

export type ImageRefClass =
  | "relative"
  | "data-uri"
  | "https"
  | "http"
  | "localhost"
  | "cid"
  | "merge-tag"
  | "empty";

export type ImageRef = {
  /** The URL exactly as it appears in the source, untrimmed. */
  raw: string;
  kind: ImageRefKind;
  classification: ImageRefClass;
  /** Offsets of `raw` within the html, for splicing. */
  start: number;
  end: number;
};

/** A Mailchimp-style merge tag, e.g. *|UNSUB|* — not a URL to resolve. */
const isMergeTag = (url: string): boolean => /^\*\|.*\|\*$/.test(url.trim());

export const classifyImageRef = (raw: string): ImageRefClass => {
  const url = raw.trim();
  if (!url) return "empty";
  if (isMergeTag(url)) return "merge-tag";
  if (/^data:/i.test(url)) return "data-uri";
  if (/^cid:/i.test(url)) return "cid";
  if (/\/\/(localhost|127\.0\.0\.1)/i.test(url)) return "localhost";
  if (/^https:\/\//i.test(url)) return "https";
  if (/^http:\/\//i.test(url)) return "http";
  // Protocol-relative "//cdn…" resolves fine in a browser but not reliably in
  // every mail client, so it is treated as absolute-insecure rather than
  // relative.
  if (/^\/\//.test(url)) return "http";
  return "relative";
};

/**
 * Each pattern captures the text BEFORE the url in group 1, so the url's start
 * offset is exactly `match.index + match[1].length`. Deriving the offset by
 * searching for the url inside the match would misfire whenever the same string
 * also appears in the preceding attributes.
 */
const PATTERNS: { kind: ImageRefKind; re: RegExp }[] = [
  // <img … src="…">
  { kind: "img-src", re: /(<img\b[^>]*?\bsrc\s*=\s*["'])([^"']*)/gi },
  // <img … srcset="a.png 1x, b.png 2x"> — the whole value; split below.
  { kind: "img-srcset", re: /(<img\b[^>]*?\bsrcset\s*=\s*["'])([^"']*)/gi },
  // <td background="…">, <table background="…"> — still standard in email.
  { kind: "background-attr", re: /(<[^>]*?\bbackground\s*=\s*["'])([^"']*)/gi },
  // url(...) in both style="" attributes and <style> blocks.
  { kind: "css-url", re: /(\burl\(\s*["']?)([^"')]*)/gi },
  // Outlook VML background fills.
  { kind: "vml-src", re: /(<v:fill\b[^>]*?\bsrc\s*=\s*["'])([^"']*)/gi },
];

/**
 * Every image reference in the document, in source order.
 *
 * srcset values are expanded into one ref per candidate so a 2x retina asset is
 * resolved and rewritten independently of the 1x.
 */
export const extractImageRefs = (html: string): ImageRef[] => {
  const refs: ImageRef[] = [];

  for (const { kind, re } of PATTERNS) {
    re.lastIndex = 0;
    for (const match of html.matchAll(re)) {
      const prefix = match[1] ?? "";
      const value = match[2] ?? "";
      const valueStart = (match.index ?? 0) + prefix.length;

      if (kind !== "img-srcset") {
        refs.push({
          raw: value,
          kind,
          classification: classifyImageRef(value),
          start: valueStart,
          end: valueStart + value.length,
        });
        continue;
      }

      // "hero.png 1x, hero@2x.png 2x" — walk candidates keeping true offsets.
      let cursor = 0;
      for (const candidate of value.split(",")) {
        const trimmedStart = candidate.length - candidate.trimStart().length;
        const urlPart = candidate.trim().split(/\s+/)[0] ?? "";
        if (urlPart) {
          const start = valueStart + cursor + trimmedStart;
          refs.push({
            raw: urlPart,
            kind,
            classification: classifyImageRef(urlPart),
            start,
            end: start + urlPart.length,
          });
        }
        cursor += candidate.length + 1; // +1 for the comma consumed by split
      }
    }
  }

  return refs.sort((a, b) => a.start - b.start);
};

/**
 * Replace url spans by offset.
 *
 * Applied right-to-left so that every not-yet-applied offset still refers to
 * the original string — patching left-to-right would shift each subsequent span
 * by the length delta of the ones before it.
 */
export const rewriteImageRefs = (
  html: string,
  replacements: { start: number; end: number; url: string }[],
): string => {
  const ordered = [...replacements].sort((a, b) => b.start - a.start);
  let out = html;
  for (const { start, end, url } of ordered) {
    out = out.slice(0, start) + url + out.slice(end);
  }
  return out;
};

export type ImageScanReport = {
  total: number;
  /** Needs a file supplied before this template can be sent. */
  unresolved: { url: string; kind: ImageRefKind }[];
  /** Inline base64 — works, but bloats the html toward Gmail's 102 KB clip. */
  dataUris: number;
  /** Must be https; http images are blocked or warned about by most clients. */
  insecure: { url: string; kind: ImageRefKind }[];
  localhost: { url: string; kind: ImageRefKind }[];
  /** Already absolute https — nothing to do, listed for review. */
  external: string[];
  ok: boolean;
};

/**
 * Preflight summary for the wizard.
 *
 * Deliberately does no network I/O. Checking whether an external https image
 * actually returns 200 needs a HEAD per URL, many CDNs refuse HEAD, and a slow
 * or blocking third party would stall the upload step — so reachability is a
 * separate, non-blocking concern rather than part of this scan.
 */
export const scanImages = (html: string): ImageScanReport => {
  const refs = extractImageRefs(html);

  const unresolved = refs
    .filter((r) => r.classification === "relative")
    .map((r) => ({ url: r.raw, kind: r.kind }));
  const insecure = refs
    .filter((r) => r.classification === "http")
    .map((r) => ({ url: r.raw, kind: r.kind }));
  const localhost = refs
    .filter((r) => r.classification === "localhost")
    .map((r) => ({ url: r.raw, kind: r.kind }));

  return {
    total: refs.length,
    unresolved,
    dataUris: refs.filter((r) => r.classification === "data-uri").length,
    insecure,
    localhost,
    external: [
      ...new Set(
        refs.filter((r) => r.classification === "https").map((r) => r.raw),
      ),
    ],
    // A relative path will not resolve in a mail client and localhost/http will
    // be blocked, so any of those means the template is not sendable yet.
    ok: unresolved.length === 0 && insecure.length === 0 && localhost.length === 0,
  };
};

/** Normalise a reference to the key used to look it up in an uploaded bundle. */
export const bundleKeyFor = (rawUrl: string): string =>
  rawUrl
    .trim()
    .split(/[?#]/)[0]!
    .replace(/^\.\//, "")
    .replace(/^\/+/, "")
    .toLowerCase();

export const IMAGE_EXTENSIONS = new Set([
  "png",
  "jpg",
  "jpeg",
  "gif",
  "webp",
  "svg",
  "ico",
]);

export const IMAGE_CONTENT_TYPES: Record<string, string> = {
  png: "image/png",
  jpg: "image/jpeg",
  jpeg: "image/jpeg",
  gif: "image/gif",
  webp: "image/webp",
  svg: "image/svg+xml",
  ico: "image/x-icon",
};

export const extensionOf = (name: string): string => {
  const clean = name.split(/[?#]/)[0] ?? "";
  const dot = clean.lastIndexOf(".");
  return dot === -1 ? "" : clean.slice(dot + 1).toLowerCase();
};
