import { logError } from "@/lib/logger";
import { EDMStorage } from "@/lib/storage/edm-storage";
import type { EDMAssetManifestEntry } from "./edm.repository";
import type { EDMBundleImage } from "./edm-import";
import {
  bundleKeyFor,
  extensionOf,
  extractImageRefs,
  IMAGE_CONTENT_TYPES,
  scanImages,
  rewriteImageRefs,
  type ImageScanReport,
} from "./edm-assets";

/**
 * Turning author-supplied html into html that will actually render in an inbox.
 *
 * The job is narrow: every image reference must end up an absolute, public,
 * https URL that will still resolve years from now. Anything relative, or
 * inlined as base64, is resolved here or reported as unresolved.
 */

const storage = EDMStorage();

export type ResolveResult = {
  html: string;
  manifest: EDMAssetManifestEntry[];
  /** Relative paths with no matching file — the wizard's "upload these" list. */
  unresolved: string[];
  report: ImageScanReport;
};

/**
 * Index bundle images under every key an author might reference them by.
 *
 * A designer writes `src="images/hero.png"`, `src="./images/hero.png"` and
 * sometimes just `src="hero.png"` for the same file, so indexing only by full
 * path would leave perfectly present images reported as missing. The basename
 * is registered only when unambiguous — two different `logo.png` in different
 * folders must not silently resolve to whichever was seen last.
 */
const indexBundleImages = (images: EDMBundleImage[]) => {
  const byPath = new Map<string, EDMBundleImage>();
  const basenameCounts = new Map<string, number>();

  for (const image of images) {
    byPath.set(image.path, image);
    const base = image.path.split("/").pop()!;
    basenameCounts.set(base, (basenameCounts.get(base) ?? 0) + 1);
  }

  for (const image of images) {
    const base = image.path.split("/").pop()!;
    if (basenameCounts.get(base) === 1 && !byPath.has(base)) {
      byPath.set(base, image);
    }
  }

  return byPath;
};

/**
 * Resolve a reference the way a browser would, against the html's own location.
 *
 * Without this, `images/hero.png` inside `welcome/index.html` looks for a
 * top-level `images/hero.png` and misses `welcome/images/hero.png`. In a
 * multi-EDM archive it is worse than a miss: `welcome/` and `reminder/` both
 * say `images/hero.png` and a flat lookup would hand them the same file.
 */
const resolveRelative = (baseDir: string, ref: string): string => {
  const cleaned = ref.trim().split(/[?#]/)[0] ?? "";
  // A leading slash means archive-root, not filesystem-root.
  const isRootAnchored = cleaned.startsWith("/");
  const segments = (isRootAnchored || !baseDir ? [] : baseDir.split("/")).concat(
    cleaned.split("/"),
  );

  const out: string[] = [];
  for (const segment of segments) {
    if (segment === "" || segment === ".") continue;
    if (segment === "..") {
      out.pop();
      continue;
    }
    out.push(segment);
  }
  return out.join("/").toLowerCase();
};

/** Decode a `data:image/png;base64,…` reference into an uploadable buffer. */
const decodeDataUri = (
  uri: string,
): { buffer: Buffer; contentType: string; ext: string } | null => {
  const match = /^data:([^;,]+)(;base64)?,(.*)$/is.exec(uri.trim());
  if (!match) return null;

  const contentType = match[1] ?? "application/octet-stream";
  if (!contentType.toLowerCase().startsWith("image/")) return null;

  const isBase64 = Boolean(match[2]);
  const payload = match[3] ?? "";

  try {
    const buffer = isBase64
      ? Buffer.from(payload, "base64")
      : Buffer.from(decodeURIComponent(payload), "utf-8");
    const ext = contentType.split("/")[1]?.split("+")[0] ?? "png";
    return { buffer, contentType, ext };
  } catch {
    return null;
  }
};

/**
 * Resolve every image reference in `html` to a public URL.
 *
 * `bundleImages` are files that arrived alongside the html (from a zip, or from
 * the wizard's fix-up step). Anything relative that is not among them comes
 * back in `unresolved` rather than being silently dropped — a broken image must
 * block the publish, not surprise someone in an inbox.
 *
 * Data URIs are extracted and uploaded too. That is not tidiness: base64 inflates
 * the html by ~33% and counts against Gmail's 102 KB clipping threshold, past
 * which the bottom of the email — usually the unsubscribe footer — is hidden
 * behind a "View entire message" link.
 */
export const resolveEDMAssets = async (
  html: string,
  bundleImages: EDMBundleImage[] = [],
  opts: { extractDataUris?: boolean; baseDir?: string } = {},
): Promise<ResolveResult> => {
  const extractDataUris = opts.extractDataUris ?? true;
  const baseDir = (opts.baseDir ?? "").replace(/^\/+|\/+$/g, "").toLowerCase();
  const byPath = indexBundleImages(bundleImages);

  const refs = extractImageRefs(html);
  const replacements: { start: number; end: number; url: string }[] = [];
  const manifest: EDMAssetManifestEntry[] = [];
  const unresolved = new Set<string>();

  // One upload per distinct source, reused across every occurrence.
  const uploadedByKey = new Map<string, { url: string; path: string; sha256: string }>();

  for (const ref of refs) {
    if (ref.classification === "relative") {
      const key = bundleKeyFor(ref.raw);
      // Most specific first: the path as the browser would resolve it, then the
      // literal archive-root path, then an unambiguous basename.
      const image =
        byPath.get(resolveRelative(baseDir, ref.raw)) ??
        byPath.get(key) ??
        byPath.get(key.split("/").pop() ?? "");

      if (!image) {
        unresolved.add(ref.raw);
        continue;
      }

      let uploaded = uploadedByKey.get(image.path);
      if (!uploaded) {
        try {
          const filename = image.path.split("/").pop() ?? "image";
          uploaded = await storage.putAsset(
            image.buffer,
            filename,
            image.contentType ||
              IMAGE_CONTENT_TYPES[extensionOf(filename)] ||
              "application/octet-stream",
          );
          uploadedByKey.set(image.path, uploaded);
          manifest.push({
            original: image.path,
            path: uploaded.path,
            url: uploaded.url,
            sha256: uploaded.sha256,
          });
        } catch (err) {
          logError(err, `[EDM] Failed uploading bundle image ${image.path}`);
          unresolved.add(ref.raw);
          continue;
        }
      }

      replacements.push({ start: ref.start, end: ref.end, url: uploaded.url });
      continue;
    }

    if (ref.classification === "data-uri" && extractDataUris) {
      const decoded = decodeDataUri(ref.raw);
      if (!decoded) continue;

      const key = `data:${decoded.buffer.length}:${decoded.contentType}`;
      let uploaded = uploadedByKey.get(key);
      if (!uploaded) {
        try {
          uploaded = await storage.putAsset(
            decoded.buffer,
            `inline.${decoded.ext}`,
            decoded.contentType,
          );
          uploadedByKey.set(key, uploaded);
          manifest.push({
            original: "(inline data uri)",
            path: uploaded.path,
            url: uploaded.url,
            sha256: uploaded.sha256,
          });
        } catch (err) {
          logError(err, "[EDM] Failed uploading inline data-uri image");
          continue;
        }
      }

      replacements.push({ start: ref.start, end: ref.end, url: uploaded.url });
    }
  }

  const resolvedHtml = rewriteImageRefs(html, replacements);

  return {
    html: resolvedHtml,
    manifest,
    unresolved: [...unresolved],
    // Re-scanned after rewriting so the report describes what will actually be
    // sent, not what was submitted.
    report: scanImages(resolvedHtml),
  };
};

/** Upload one image on its own — the wizard's fix-up step for missing files. */
export const uploadEDMAsset = async (
  buffer: Buffer,
  filename: string,
  contentType: string,
  folderPath?: string,
): Promise<EDMAssetManifestEntry> => {
  const uploaded = await storage.putAsset(
    buffer,
    filename,
    contentType ||
      IMAGE_CONTENT_TYPES[extensionOf(filename)] ||
      "application/octet-stream",
    { folderPath },
  );
  return {
    original: filename,
    path: uploaded.path,
    url: uploaded.url,
    sha256: uploaded.sha256,
  };
};
