/**
 * RustyImage: Bilder über RustyCMS /api/transform laden, in public speichern, lokal ausliefern.
 * Hash aus URL + Transform-Parametern, damit gleiche Anfrage nicht erneut geladen wird.
 * Nur für Build/SSR (Node) – nutzt fs und fetch zur CMS-Basis-URL.
 */

import { createHash } from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import {
  absoluteUrlFromCmsImageField,
  getCmsBaseUrl,
  normalizeMediaUrl,
} from "./cms-media-url";

/** Für ältere Importe von `rusty-image`; neue Nutzung: `./cms-media-url`. */
export { normalizeMediaUrl };

/** Transform-Parameter wie von der RustyCMS-API unterstützt. */
export interface RustyImageTransformParams {
  width?: number;
  height?: number;
  /** Aspect ratio vor Skalierung, z. B. "1:1" oder "16:9" (Mittelpunkt-Crop). */
  ar?: string;
  /** fill | contain | cover */
  fit?: "fill" | "contain" | "cover";
  /** Ausgabeformat */
  format?: "jpeg" | "png" | "webp" | "avif";
  /** JPEG-Qualität 1–100 */
  quality?: number;
}

const DEFAULT_FORMAT = "jpeg";
const SUBDIR = "images/transformed";
/** Persistenter Cache für Builds – überlebt "astro build" (dist wird geleert). */
const TRANSFORM_CACHE_DIR = ".cache/transformed-images";

/** Stabile Dateiendung aus format. */
function formatToExt(format: string): string {
  switch (format.toLowerCase()) {
    case "png":
      return "png";
    case "webp":
      return "webp";
    case "avif":
      return "avif";
    default:
      return "jpg";
  }
}

/**
 * Erzeugt einen stabilen Hash aus URL und allen Transform-Parametern.
 * Gleiche URL + gleiche Params → gleicher Hash → keine erneute Transformation.
 */
export function hashForTransform(
  url: string,
  params: RustyImageTransformParams = {},
): string {
  const normalized = {
    url,
    w: params.width,
    h: params.height,
    ar: params.ar,
    fit: params.fit ?? "contain",
    format: params.format ?? DEFAULT_FORMAT,
    quality: params.quality ?? 85,
  };
  const payload = JSON.stringify(normalized);
  return createHash("sha256").update(payload).digest("hex").slice(0, 20);
}

/**
 * Stellt sicher, dass das transformierte Bild in public existiert.
 * Wenn nicht: GET an RustyCMS /api/transform, Ergebnis in public/images/transformed/<hash>.<ext> schreiben.
 * Gibt den öffentlichen Pfad zurück (z. B. /images/transformed/abc123def456.jpg).
 */
export async function ensureTransformedImage(
  url: string,
  params: RustyImageTransformParams = {},
): Promise<string> {
  const absoluteUrl = normalizeMediaUrl(url);
  const format = params.format ?? DEFAULT_FORMAT;
  const ext = formatToExt(format);
  const hash = hashForTransform(absoluteUrl, params);
  const { width, height } = params;
  const dimSuffix =
    width != null && height != null
      ? `-${width}x${height}`
      : width != null
        ? `-${width}w`
        : height != null
          ? `-${height}h`
          : "";

  const projectRoot =
    typeof process !== "undefined" && process.cwd ? process.cwd() : undefined;
  if (!projectRoot) {
    throw new Error("rusty-image: project root (process.cwd) not available");
  }

  // Build: in dist schreiben, damit Preview/Deploy die Bilder ausliefert (public wird vor dem Rendern kopiert).
  // Dev: in public schreiben, damit der Dev-Server sie ausliefert.
  const isBuild = typeof import.meta !== "undefined" && (import.meta as { env?: { PROD?: boolean } }).env?.PROD === true;
  const baseDir = isBuild ? path.join(projectRoot, "dist") : path.join(projectRoot, "public");
  const dir = path.join(baseDir, SUBDIR);
  const filename = `${hash}${dimSuffix}.${ext}`;
  const filePath = path.join(dir, filename);
  const publicPath = `/${SUBDIR}/${filename}`;

  // Build: zuerst persistenten Cache prüfen (dist wird bei jedem Build geleert).
  const cacheDir = path.join(projectRoot, TRANSFORM_CACHE_DIR);
  const cacheFilePath = path.join(cacheDir, filename);
  if (isBuild) {
    try {
      await fs.access(cacheFilePath);
      await fs.mkdir(dir, { recursive: true });
      await fs.copyFile(cacheFilePath, filePath);
      return publicPath;
    } catch {
      /* nicht im Cache, laden und in Cache + dist schreiben */
    }
  } else {
    try {
      await fs.access(filePath);
      return publicPath;
    } catch {
      /* Datei existiert nicht, laden und schreiben */
    }
  }

  const baseUrl = getCmsBaseUrl();
  const searchParams = new URLSearchParams();
  searchParams.set("url", absoluteUrl);
  if (params.width != null) searchParams.set("w", String(params.width));
  if (params.height != null) searchParams.set("h", String(params.height));
  if (params.ar != null) searchParams.set("ar", params.ar);
  if (params.fit != null) searchParams.set("fit", params.fit);
  if (params.format != null) searchParams.set("format", params.format);
  if (params.quality != null)
    searchParams.set("quality", String(params.quality));

  const transformUrl = `${baseUrl}/api/transform?${searchParams.toString()}`;
  console.log("[rusty-image] GET", transformUrl);
  const res = await fetch(transformUrl);
  if (!res.ok) {
    throw new Error(
      `rusty-image: transform failed ${res.status} for ${absoluteUrl}: ${await res.text()}`,
    );
  }

  const buffer = Buffer.from(await res.arrayBuffer());
  await fs.mkdir(dir, { recursive: true });
  await fs.writeFile(filePath, buffer);
  if (isBuild) {
    await fs.mkdir(cacheDir, { recursive: true });
    await fs.writeFile(cacheFilePath, buffer);
  }

  return publicPath;
}

/** Ein Eintrag aus einem Bild-Array: URL-String oder aufgelöstes img-Objekt (src). */
type ImageArrayItem = string | { src?: string };

/**
 * Löst ein Array von Bild-URLs/Referenzen über die Transform-API auf.
 * Akzeptiert string[] oder Objekte mit src (z. B. { _type: "img", src, description }).
 * Gibt ein Array von öffentlichen Pfaden zurück (z. B. für Fullwidth-Banner).
 */
export async function resolveImageUrls(
  items: ImageArrayItem[],
  params: RustyImageTransformParams = {},
): Promise<string[]> {
  const result: string[] = [];
  for (const item of items) {
    const u = absoluteUrlFromCmsImageField(item);
    if (!u) continue;
    try {
      result.push(await ensureTransformedImage(u, params));
    } catch (e) {
      console.warn("[rusty-image] resolve failed for", u, e);
    }
  }
  return result;
}

/** Holt die Bild-URL aus einem Image-Block (image oder img: string, src oder file.url). */
function getImageBlockUrl(block: {
  image?: string | { src?: string; file?: { url?: string }; title?: string };
  img?: string | { src?: string; file?: { url?: string }; title?: string };
}): string | null {
  return absoluteUrlFromCmsImageField(block.image ?? block.img);
}

/** URL aus einem Galerie-Bildeintrag (src oder file.url). */
function getGalleryImageUrl(
  img: { src?: string; file?: { url?: string } },
): string | null {
  return absoluteUrlFromCmsImageField(img);
}

/** RowContentLayout-typ: Zeilen mit content-Arrays. */
export interface RowContentLayoutLike {
  row1Content?: unknown[];
  row2Content?: unknown[];
  row3Content?: unknown[];
}

/** Parameter für resolveContentImages (optional baseUrl für Markdown-Bild-Links). */
export interface ResolveContentImagesOptions {
  transformParams?: RustyImageTransformParams;
  /** Basis-URL für Links auf Bilder (z. B. Astro.url.origin oder SITE). Fehlt → aus import.meta.env.SITE. */
  baseUrl?: string;
}

/** Markdown-Inline-Bilder: 1200px Breite, WebP, Link öffnet dasselbe Bild in neuem Tab. */
const MARKDOWN_IMAGE_PARAMS: RustyImageTransformParams = {
  width: 1200,
  fit: "contain",
  format: "webp",
  quality: 85,
};

const IMG_SRC_REGEX =
  /<img\s+([^>]*?)src\s*=\s*["']([^"']+)["']([^>]*)>/gi;

/**
 * Ersetzt in HTML alle <img src="...">: Bild-URL wird transformiert (RustyImage),
 * Bild wird in <a href="..." target="_blank"> eingewickelt (öffnet Bild in neuem Tab).
 * href ist relativ (z. B. /images/transformed/…), damit auf localhost und Production dieselbe Seite funktioniert.
 */
export async function processMarkdownHtmlImages(
  html: string,
  _baseUrl?: string,
  transformParams: RustyImageTransformParams = MARKDOWN_IMAGE_PARAMS,
): Promise<string> {
  const matches = [...html.matchAll(IMG_SRC_REGEX)];
  const replacements: { index: number; length: number; newBlock: string }[] = [];
  for (const m of matches) {
    const rawSrc = m[2];
    const url = rawSrc.startsWith("//") ? `https:${rawSrc}` : rawSrc;
    const absolute = normalizeMediaUrl(url);
    if (!absolute.startsWith("http")) continue;
    try {
      const transformedPath = await ensureTransformedImage(absolute, transformParams);
      const href = transformedPath.startsWith("/") ? transformedPath : `/${transformedPath}`;
      const newImg = m[0].replace(rawSrc, transformedPath);
      const newBlock = `<a href="${href}" target="_blank" rel="noopener noreferrer" class="markdown-image-link">${newImg}</a>`;
      replacements.push({ index: m.index ?? 0, length: m[0].length, newBlock });
    } catch (e) {
      console.warn("[rusty-image] markdown img resolve failed for", url, e);
    }
  }
  if (replacements.length === 0) return html;
  replacements.sort((a, b) => b.index - a.index);
  let out = html;
  for (const r of replacements) {
    out = out.slice(0, r.index) + r.newBlock + out.slice(r.index + r.length);
  }
  return out;
}

const RAW_SUBDIR = "images/raw";
const RAW_CACHE_DIR = ".cache/raw-assets";

/**
 * Lädt ein CMS-Asset direkt herunter (ohne Transform) und legt es lokal ab.
 * Gibt den lokalen öffentlichen Pfad zurück (z.B. /images/raw/abc123.pdf).
 */
export async function downloadAsset(url: string): Promise<string> {
  const absoluteUrl = url.trim();
  if (!absoluteUrl.startsWith("http")) return url;

  // _environment und _locale aus URL entfernen für sauberen Hash und Dateinamen
  let cleanUrl = absoluteUrl;
  try {
    const u = new URL(absoluteUrl);
    u.searchParams.delete("_environment");
    u.searchParams.delete("_locale");
    cleanUrl = u.toString();
  } catch { /* ignore */ }

  const hash = createHash("sha256").update(cleanUrl).digest("hex").slice(0, 24);
  const rawExt = cleanUrl.split("?")[0].split(".").pop()?.toLowerCase() ?? "bin";
  const filename = `${hash}.${rawExt}`;

  const projectRoot = typeof process !== "undefined" && process.cwd ? process.cwd() : undefined;
  if (!projectRoot) return url;

  const isBuild = typeof import.meta !== "undefined" && (import.meta as { env?: { PROD?: boolean } }).env?.PROD === true;
  const baseDir = isBuild ? path.join(projectRoot, "dist") : path.join(projectRoot, "public");
  const dir = path.join(baseDir, RAW_SUBDIR);
  const filePath = path.join(dir, filename);
  const publicPath = `/${RAW_SUBDIR}/${filename}`;
  const cacheDir = path.join(projectRoot, RAW_CACHE_DIR);
  const cacheFilePath = path.join(cacheDir, filename);

  if (isBuild) {
    try {
      await fs.access(cacheFilePath);
      await fs.mkdir(dir, { recursive: true });
      await fs.copyFile(cacheFilePath, filePath);
      return publicPath;
    } catch { /* nicht im Cache */ }
  } else {
    try {
      await fs.access(filePath);
      return publicPath;
    } catch { /* Datei existiert nicht */ }
  }

  console.log("[rusty-image] GET (raw)", absoluteUrl);
  const res = await fetch(absoluteUrl);
  if (!res.ok) throw new Error(`downloadAsset: fetch failed ${res.status} for ${absoluteUrl}`);

  const buffer = Buffer.from(await res.arrayBuffer());
  await fs.mkdir(dir, { recursive: true });
  await fs.writeFile(filePath, buffer);
  if (isBuild) {
    await fs.mkdir(cacheDir, { recursive: true });
    await fs.writeFile(cacheFilePath, buffer);
  }
  return publicPath;
}

const A_HREF_REGEX = /<a(\s[^>]*?)href\s*=\s*["']([^"']+)["']([^>]*)>/gi;

/**
 * Findet alle <a href="..."> die auf CMS-Assets zeigen (/api/assets/…),
 * lädt die Datei lokal herunter und ersetzt die href mit dem lokalen Pfad.
 */
export async function downloadCmsAssetLinks(html: string): Promise<string> {
  const matches = [...html.matchAll(A_HREF_REGEX)];
  const replacements: { index: number; length: number; newTag: string }[] = [];
  for (const m of matches) {
    const rawHref = m[2];
    const absolute = normalizeMediaUrl(rawHref);
    if (!absolute.startsWith("http") || !absolute.includes("/api/assets/")) continue;
    try {
      const localPath = await downloadAsset(absolute);
      const newTag = m[0].replace(rawHref, localPath);
      replacements.push({ index: m.index ?? 0, length: m[0].length, newTag });
    } catch (e) {
      console.warn("[rusty-image] asset link download failed for", rawHref, e);
    }
  }
  if (replacements.length === 0) return html;
  replacements.sort((a, b) => b.index - a.index);
  let out = html;
  for (const r of replacements) {
    out = out.slice(0, r.index) + r.newTag + out.slice(r.index + r.length);
  }
  return out;
}

/**
 * Verarbeitet das Top-Level `content`-Feld (Markdown-Body) eines Post/Page-Eintrags:
 * Markdown → HTML → Bilder transformieren → Asset-Links lokalisieren.
 * Gibt fertiges HTML zurück (oder "" wenn kein content vorhanden).
 */
export async function resolveBodyContent(
  entry: { content?: unknown },
): Promise<string> {
  const raw = typeof entry.content === "string" ? entry.content.trim() : "";
  if (!raw) return "";
  const { marked } = await import("marked");
  marked.setOptions({ gfm: true });
  const html = marked.parse(raw) as string;
  return downloadCmsAssetLinks(await processMarkdownHtmlImages(html));
}

/**
 * Geht alle Content-Rows durch: Image-Blöcke → resolvedImageSrc;
 * Markdown-Blöcke → resolvedContent (HTML mit transformierten Bildern + Link-Wrapper).
 */
export async function resolveContentImages(
  layout: RowContentLayoutLike,
  transformParamsOrOptions: RustyImageTransformParams | ResolveContentImagesOptions = {},
): Promise<void> {
  const options: ResolveContentImagesOptions =
    transformParamsOrOptions && "baseUrl" in transformParamsOrOptions
      ? (transformParamsOrOptions as ResolveContentImagesOptions)
      : { transformParams: transformParamsOrOptions as RustyImageTransformParams };
  const transformParams = options.transformParams ?? {};

  const rows = [
    layout.row1Content,
    layout.row2Content,
    layout.row3Content,
  ].filter((r): r is unknown[] => Array.isArray(r));

  /**
   * Galerie: max. Breite 1280, Seitenverhältnis erhalten (contain + nur w).
   * Feste 16:9-Crops würden alle Bilder gleich groß machen — schlecht für Masonry/Album-Layout.
   */
  const galleryParams: RustyImageTransformParams = {
    width: 1280,
    fit: "contain",
    format: "webp",
    quality: 80,
    ...transformParams,
  };

  const { marked } = await import("marked");
  marked.setOptions({ gfm: true });

  for (const content of rows) {
    for (const item of content) {
      if (typeof item !== "object" || item === null) continue;
      const block = item as {
        _type?: string;
        content?: string;
        image?: unknown;
        img?: unknown;
        images?: Array<{ src?: string; file?: { url?: string }; resolvedSrc?: string }>;
        resolvedImageSrc?: string;
        resolvedContent?: string;
      };
      if (block._type === "markdown" && typeof block.content === "string" && block.content.trim()) {
        try {
          const html = marked.parse(block.content) as string;
          block.resolvedContent = await downloadCmsAssetLinks(await processMarkdownHtmlImages(html));
        } catch (e) {
          console.warn("[rusty-image] markdown block resolve failed", e);
        }
        continue;
      }
      if (block._type === "image") {
        const url = getImageBlockUrl(
          block as {
            image?: string | { file?: { url?: string }; title?: string };
            img?: string | { file?: { url?: string }; title?: string };
          },
        );
        if (!url) continue;
        try {
          block.resolvedImageSrc = await ensureTransformedImage(
            url,
            transformParams,
          );
        } catch (e) {
          console.warn("[rusty-image] resolve failed for", url, e);
        }
        continue;
      }
      if (block._type === "image_gallery" && Array.isArray(block.images)) {
        for (const img of block.images) {
          if (!img || typeof img !== "object") continue;
          const url = getGalleryImageUrl(img);
          if (!url) continue;
          try {
            (img as { resolvedSrc?: string }).resolvedSrc =
              await ensureTransformedImage(url, galleryParams);
          } catch (e) {
            console.warn("[rusty-image] gallery resolve failed for", url, e);
          }
        }
        continue;
      }
      if (block._type === "organisations" && Array.isArray(block.organisations)) {
        const orgLogoParams: RustyImageTransformParams = {
          width: 256,
          fit: "contain",
          format: "webp",
          quality: 85,
          ...transformParams,
        };
        for (const org of block.organisations) {
          if (!org || typeof org !== "object") continue;
          const o = org as {
            logo?: unknown;
            resolvedLogoSrc?: string;
          };
          const url = absoluteUrlFromCmsImageField(o.logo);
          if (!url) continue;
          try {
            o.resolvedLogoSrc = await ensureTransformedImage(url, orgLogoParams);
          } catch (e) {
            console.warn(
              "[rusty-image] organisation logo resolve failed for",
              url,
              e,
            );
          }
        }
        continue;
      }
    }
  }
}
