import { NextResponse } from "next/server";
import dns from "node:dns/promises";
import net from "node:net";
import { getClientIp } from "@/lib/auth";
import { checkRateLimit } from "@/lib/security";

export const runtime = "nodejs";

const UA =
  "Mozilla/5.0 (compatible; ToolaroFaviconBot/1.0; +https://toolaro.org/tools/favicon-tester)";
const MAX_ICON_PROBES = 14;
const MAX_HTML_BYTES = 400_000;
const MAX_ICON_BYTES = 3_000_000;

type IconRow = {
  rel: string;
  kind: "icon" | "apple" | "mask" | "android";
  href: string;
  declaredSizes: string;
  declaredType: string;
  source: "head" | "manifest" | "implicit";
  color?: string;
  w: number;
  h: number;
  bytes: number;
  reachable: boolean;
  square: boolean | null;
  imgType: string;
  note?: string;
};

type Check = { id: string; label: string; status: "pass" | "warn" | "fail"; msg: string; fix?: string };

export async function POST(request: Request) {
  try {
    const ip = getClientIp();
    const limited = await checkRateLimit({ key: ip, action: "favicon-tester", limit: 30, windowSeconds: 60 });
    if (!limited.ok) {
      return NextResponse.json({ ok: false, message: "Zu viele Anfragen. Bitte spaeter erneut versuchen." }, { status: 429 });
    }

    const body = (await request.json().catch(() => ({}))) as { url?: string };
    const raw = String(body.url ?? "").trim().slice(0, 2048);
    const url = normalize(raw);
    if (!url) return NextResponse.json({ ok: false, message: "Bitte eine gueltige http(s)-Adresse eingeben." }, { status: 422 });
    if (!(await isPublicHttpUrl(url)))
      return NextResponse.json({ ok: false, message: "Diese Adresse ist nicht erlaubt (lokale oder interne Hosts werden blockiert)." }, { status: 422 });

    const page = await fetchFollow(url, { timeoutMs: 8000 });
    if (!page) return NextResponse.json({ ok: false, message: "Seite nicht erreichbar oder zu viele Weiterleitungen." });

    const baseUrl = page.url;
    const org = origin(baseUrl);
    const html = (await safeText(page.res)).slice(0, MAX_HTML_BYTES);
    const parsed = parseHead(html);

    // Kandidaten sammeln
    const candidates = new Map<string, IconRow>();
    for (const ic of parsed.icons) {
      const abs = absolutize(ic.href, baseUrl);
      if (!abs) continue;
      if (!candidates.has(abs)) {
        candidates.set(abs, {
          rel: ic.rel, kind: ic.kind, href: abs, declaredSizes: ic.sizes, declaredType: ic.type,
          source: "head", color: ic.color, w: 0, h: 0, bytes: 0, reachable: false, square: null, imgType: "",
        });
      }
    }
    const icoFallback = org + "/favicon.ico";
    if (!candidates.has(icoFallback)) {
      candidates.set(icoFallback, {
        rel: "icon", kind: "icon", href: icoFallback, declaredSizes: "", declaredType: "",
        source: "implicit", w: 0, h: 0, bytes: 0, reachable: false, square: null, imgType: "",
      });
    }

    // Manifest
    let manifest: ManifestInfo | null = null;
    if (parsed.manifest) {
      const manUrl = absolutize(parsed.manifest, baseUrl);
      if (manUrl && (await isPublicHttpUrl(manUrl))) {
        manifest = await loadManifest(manUrl);
        if (manifest?.icons?.length) {
          for (const mi of manifest.icons) {
            const abs = absolutize(mi.src, manUrl);
            if (!abs || candidates.has(abs)) continue;
            candidates.set(abs, {
              rel: "manifest", kind: "android", href: abs, declaredSizes: mi.sizes, declaredType: mi.type,
              source: "manifest", w: 0, h: 0, bytes: 0, reachable: false, square: null, imgType: "",
            });
          }
        }
      }
    }

    // Icons pruefen
    const icons: IconRow[] = [];
    let probed = 0;
    for (const c of candidates.values()) {
      if (probed >= MAX_ICON_PROBES) break;
      probed++;
      icons.push(await probeIcon(c));
    }

    const checks = buildChecks(icons, manifest, parsed);
    const score = scoreOf(checks, icons);

    return NextResponse.json({
      ok: true,
      site: {
        input: raw,
        final: baseUrl,
        origin: org,
        title: parsed.title || hostLabel(org),
        appName: parsed.appName || manifest?.short_name || manifest?.name || null,
        themeColor: parsed.themeColor,
        ogImage: parsed.ogImage ? absolutize(parsed.ogImage, baseUrl) : null,
      },
      icons,
      manifest,
      checks,
      score,
      preview: buildPreview(icons, manifest, parsed, org),
      recommendedHead: recommendedHead(parsed),
    });
  } catch (err) {
    console.error("[favicon-tester]", err);
    return NextResponse.json({ ok: false, message: "Interner Fehler bei der Pruefung." }, { status: 500 });
  }
}

/* ----------------------------------------------------------------- Parsing */

type ParsedHead = {
  title: string | null;
  appName: string | null;
  themeColor: string | null;
  ogImage: string | null;
  manifest: string | null;
  icons: { rel: string; kind: IconRow["kind"]; href: string; sizes: string; type: string; color?: string }[];
};

function attrs(tag: string): Record<string, string> {
  const out: Record<string, string> = {};
  const re = /([a-zA-Z:-]+)\s*=\s*("([^"]*)"|'([^']*)'|([^\s"'>]+))/g;
  let m: RegExpExecArray | null;
  // HTML-Entities dekodieren – z. B. href="...?w=96&amp;h=96" muss zu &h=96 werden,
  // sonst schickt man dem Server den Query-Parameter "amp;h" und kassiert einen 400.
  while ((m = re.exec(tag))) out[m[1].toLowerCase()] = decodeEntities((m[3] ?? m[4] ?? m[5] ?? "").trim());
  return out;
}

function parseHead(html: string): ParsedHead {
  const out: ParsedHead = { title: null, appName: null, themeColor: null, ogImage: null, manifest: null, icons: [] };

  const t = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
  if (t) out.title = decodeEntities(t[1]).replace(/\s+/g, " ").trim();

  for (const m of html.matchAll(/<link\b[^>]*>/gi)) {
    const a = attrs(m[0]);
    const rel = (a.rel || "").toLowerCase();
    const href = a.href;
    if (!rel || !href) continue;
    if (rel === "manifest") { out.manifest = href; continue; }
    if (rel.includes("apple-touch-icon")) { out.icons.push({ rel, kind: "apple", href, sizes: (a.sizes || "").toLowerCase(), type: (a.type || "").toLowerCase() }); continue; }
    if (rel === "mask-icon") { out.icons.push({ rel, kind: "mask", href, sizes: "", type: "image/svg+xml", color: a.color }); continue; }
    if (rel.includes("icon")) { out.icons.push({ rel, kind: "icon", href, sizes: (a.sizes || "").toLowerCase(), type: (a.type || "").toLowerCase() }); continue; }
  }

  for (const m of html.matchAll(/<meta\b[^>]*>/gi)) {
    const a = attrs(m[0]);
    const name = (a.name || "").toLowerCase();
    const prop = (a.property || "").toLowerCase();
    const content = a.content;
    if (!content) continue;
    if (name === "theme-color" && !out.themeColor) out.themeColor = content;
    if ((name === "application-name" || name === "apple-mobile-web-app-title") && !out.appName) out.appName = content;
    if (prop === "og:image" && !out.ogImage) out.ogImage = content;
  }

  return out;
}

type ManifestInfo = {
  url: string;
  ok: boolean;
  name?: string | null;
  short_name?: string | null;
  theme_color?: string | null;
  background_color?: string | null;
  iconCount: number;
  has192: boolean;
  has512: boolean;
  icons: { src: string; sizes: string; type: string; purpose: string }[];
};

async function loadManifest(manUrl: string): Promise<ManifestInfo | null> {
  const res = await fetchFollow(manUrl, { maxRedirects: 3, timeoutMs: 6000 });
  if (!res || res.status >= 400) return null;
  let json: unknown;
  try { json = JSON.parse((await safeText(res.res)).slice(0, 200_000)); } catch { return { url: manUrl, ok: false, iconCount: 0, has192: false, has512: false, icons: [] }; }
  const obj = (json ?? {}) as Record<string, unknown>;
  const rawIcons = Array.isArray(obj.icons) ? (obj.icons as Record<string, unknown>[]) : [];
  const icons = rawIcons
    .filter((i) => i && typeof i.src === "string")
    .map((i) => ({ src: String(i.src), sizes: String(i.sizes ?? "").toLowerCase(), type: String(i.type ?? "").toLowerCase(), purpose: String(i.purpose ?? "") }));
  const allSizes = icons.map((i) => i.sizes).join(" ");
  return {
    url: manUrl, ok: true,
    name: typeof obj.name === "string" ? obj.name : null,
    short_name: typeof obj.short_name === "string" ? obj.short_name : null,
    theme_color: typeof obj.theme_color === "string" ? obj.theme_color : null,
    background_color: typeof obj.background_color === "string" ? obj.background_color : null,
    iconCount: icons.length,
    has192: allSizes.includes("192x192"),
    has512: allSizes.includes("512x512"),
    icons,
  };
}

/* -------------------------------------------------------------- Icon probe */

async function probeIcon(c: IconRow): Promise<IconRow> {
  if (c.href.toLowerCase().startsWith("data:")) {
    return { ...c, reachable: true, imgType: "data", note: "Inline (data URI)" };
  }
  if (!(await isPublicHttpUrl(c.href))) return { ...c, note: "Adresse blockiert" };

  const res = await fetchFollow(c.href, { maxRedirects: 3, timeoutMs: 6000, bin: true });
  if (!res) return c;
  if (res.status >= 400) return { ...c, /* status */ };

  const len = Number(res.res.headers.get("content-length") || "0");
  if (len > MAX_ICON_BYTES) return { ...c, reachable: true, bytes: len };

  let buf: Buffer;
  try { buf = Buffer.from(await res.res.arrayBuffer()); } catch { return c; }
  if (!buf.length) return c;

  const { w, h, type } = imageDims(buf, c.href);
  return { ...c, reachable: true, bytes: buf.length, w, h, imgType: type, square: w > 0 && h > 0 && type !== "svg" ? w === h : null };
}

function imageDims(b: Buffer, href: string): { w: number; h: number; type: string } {
  const a = (i: number, n: number) => b.toString("latin1", i, i + n);

  if (b.length > 24 && b[0] === 0x89 && a(1, 3) === "PNG") return { w: b.readUInt32BE(16), h: b.readUInt32BE(20), type: "png" };
  if (b.length > 10 && a(0, 3) === "GIF") return { w: b.readUInt16LE(6), h: b.readUInt16LE(8), type: "gif" };
  if (b.length > 26 && a(0, 2) === "BM") return { w: b.readInt32LE(18), h: Math.abs(b.readInt32LE(22)), type: "bmp" };

  if (b.length > 30 && a(0, 4) === "RIFF" && a(8, 4) === "WEBP") {
    const fmt = a(12, 4);
    try {
      if (fmt === "VP8X") return { w: (b.readUIntLE(24, 3) & 0xffffff) + 1, h: (b.readUIntLE(27, 3) & 0xffffff) + 1, type: "webp" };
      if (fmt === "VP8 ") return { w: b.readUInt16LE(26) & 0x3fff, h: b.readUInt16LE(28) & 0x3fff, type: "webp" };
    } catch { /* fallthrough */ }
    return { w: 0, h: 0, type: "webp" };
  }

  if ((b[0] === 0 && b[1] === 0 && b[2] === 1 && b[3] === 0) || /\.ico(\?|$)/i.test(href)) {
    const count = b.readUInt16LE(4);
    let best = 0, bw = 0, bh = 0;
    for (let i = 0; i < count; i++) {
      const off = 6 + i * 16;
      if (off + 2 > b.length) break;
      const w = b[off] || 256, h = b[off + 1] || 256;
      if (w * h > best) { best = w * h; bw = w; bh = h; }
    }
    return { w: bw, h: bh, type: "ico" };
  }

  if (b[0] === 0xff && b[1] === 0xd8) {
    let i = 2;
    while (i < b.length - 8) {
      if (b[i] !== 0xff) { i++; continue; }
      const marker = b[i + 1];
      if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
        return { w: b.readUInt16BE(i + 7), h: b.readUInt16BE(i + 5), type: "jpeg" };
      }
      i += 2 + b.readUInt16BE(i + 2);
    }
    return { w: 0, h: 0, type: "jpeg" };
  }

  const txt = b.toString("utf8", 0, Math.min(b.length, 4000));
  if (txt.includes("<svg")) return { ...svgDims(txt), type: "svg" };

  return { w: 0, h: 0, type: "" };
}

function svgDims(svg: string): { w: number; h: number } {
  const w = svg.match(/<svg[^>]*\bwidth=["']?(\d+(?:\.\d+)?)/i);
  const h = svg.match(/<svg[^>]*\bheight=["']?(\d+(?:\.\d+)?)/i);
  if (w && h) return { w: Math.round(+w[1]), h: Math.round(+h[1]) };
  const vb = svg.match(/viewBox=["']?\s*[\d.-]+\s+[\d.-]+\s+([\d.]+)\s+([\d.]+)/i);
  if (vb) return { w: Math.round(+vb[1]), h: Math.round(+vb[2]) };
  return { w: 0, h: 0 };
}

/* ----------------------------------------------------------- Checks/Score */

function buildChecks(icons: IconRow[], manifest: ManifestInfo | null, parsed: ParsedHead): Check[] {
  const reachable = icons.filter((i) => i.reachable);
  const classic = reachable.filter((i) => i.kind === "icon" && i.imgType !== "svg");
  const svg = reachable.filter((i) => i.imgType === "svg" && i.kind !== "mask");
  const apple = reachable.filter((i) => i.kind === "apple");
  const badSquare = reachable.filter((i) => i.square === false);
  const broken = icons.filter((i) => i.source !== "implicit" && !i.reachable);

  const checks: Check[] = [];

  checks.push(classic.length || svg.length
    ? { id: "favicon", label: "Favicon (Browser-Tab)", status: "pass", msg: "Ein Favicon fuer den Browser-Tab ist vorhanden und erreichbar." }
    : { id: "favicon", label: "Favicon (Browser-Tab)", status: "fail", msg: "Kein erreichbares Favicon gefunden - der Browser zeigt ein leeres Icon.", fix: '<link rel="icon" href="/favicon.ico" sizes="any">' });

  checks.push(svg.length
    ? { id: "svg", label: "SVG-Favicon (scharf & Dark-Mode)", status: "pass", msg: "Ein skalierbares SVG-Favicon ist vorhanden - bleibt auf jedem Display scharf." }
    : { id: "svg", label: "SVG-Favicon (scharf & Dark-Mode)", status: "warn", msg: "Kein SVG-Favicon. Empfohlen fuer gestochen scharfe Tabs und Dark-Mode-Varianten.", fix: '<link rel="icon" type="image/svg+xml" href="/favicon.svg">' });

  if (apple.length) {
    const best = largest(apple)!;
    checks.push(best.w && best.w < 152
      ? { id: "apple", label: "Apple-Touch-Icon (iPhone/iPad)", status: "warn", msg: `Apple-Touch-Icon ist nur ${best.w}x${best.h} px. Empfohlen sind 180x180 px.`, fix: '<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">' }
      : { id: "apple", label: "Apple-Touch-Icon (iPhone/iPad)", status: "pass", msg: "Apple-Touch-Icon vorhanden - sieht beim Speichern auf dem iOS-Homescreen sauber aus." });
  } else {
    checks.push({ id: "apple", label: "Apple-Touch-Icon (iPhone/iPad)", status: "warn", msg: "Kein Apple-Touch-Icon. iOS zeigt sonst einen unscharfen Screenshot der Seite.", fix: '<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">' });
  }

  if (manifest?.ok) {
    checks.push(manifest.has192 && manifest.has512
      ? { id: "manifest", label: "Web-App-Manifest (Android/PWA)", status: "pass", msg: "Manifest mit 192er- und 512er-Icon vorhanden - ideal fuer Android & Installierbarkeit." }
      : { id: "manifest", label: "Web-App-Manifest (Android/PWA)", status: "warn", msg: "Manifest vorhanden, aber es fehlt ein 192x192- oder 512x512-Icon fuer Android.", fix: '"icons":[{"src":"/icon-192.png","sizes":"192x192","type":"image/png"},{"src":"/icon-512.png","sizes":"512x512","type":"image/png"}]' });
  } else {
    checks.push({ id: "manifest", label: "Web-App-Manifest (Android/PWA)", status: "warn", msg: "Kein Web-App-Manifest. Fuer Android-Homescreen und PWA-Installation empfohlen.", fix: '<link rel="manifest" href="/site.webmanifest">' });
  }

  checks.push(parsed.themeColor
    ? { id: "theme", label: "Theme-Color (Browser-UI)", status: "pass", msg: `theme-color gesetzt (${parsed.themeColor}) - faerbt die Browserleiste auf Mobilgeraeten.` }
    : { id: "theme", label: "Theme-Color (Browser-UI)", status: "warn", msg: "Keine theme-color. Die mobile Adressleiste bleibt neutral statt in deiner Markenfarbe.", fix: '<meta name="theme-color" content="#0f172a">' });

  checks.push(badSquare.length
    ? { id: "square", label: "Quadratische Icons", status: "fail", msg: `Mindestens ein Icon ist nicht quadratisch (${badSquare[0].w}x${badSquare[0].h} px) und wird verzerrt dargestellt.` }
    : { id: "square", label: "Quadratische Icons", status: "pass", msg: "Alle geprueften Icons sind quadratisch." });

  checks.push(broken.length
    ? { id: "reach", label: "Alle Icons erreichbar", status: "fail", msg: `${broken.length} deklariertes Icon ist nicht erreichbar (404 o. ae.) - toter Link im <head>.` }
    : { id: "reach", label: "Alle Icons erreichbar", status: "pass", msg: "Alle deklarierten Icons liefern eine gueltige Datei." });

  return checks;
}

function scoreOf(checks: Check[], icons: IconRow[]): number {
  let s = 100;
  for (const c of checks) { if (c.status === "fail") s -= 18; if (c.status === "warn") s -= 7; }
  if (!icons.some((i) => i.reachable && i.kind !== "mask")) s = Math.min(s, 35);
  return Math.max(0, Math.min(100, s));
}

function buildPreview(icons: IconRow[], manifest: ManifestInfo | null, parsed: ParsedHead, org: string) {
  const reach = icons.filter((i) => i.reachable && i.kind !== "mask");
  const svg = reach.find((i) => i.imgType === "svg");
  const png = largest(reach.filter((i) => ["png", "jpeg", "webp", "gif"].includes(i.imgType) && i.kind === "icon"));
  const apple = largest(reach.filter((i) => i.kind === "apple"));
  const android = largest(reach.filter((i) => i.kind === "android"));
  const anyRaster = largest(reach.filter((i) => i.imgType !== "svg"));
  const ico = reach.find((i) => i.imgType === "ico");

  const tab = svg?.href ?? png?.href ?? ico?.href ?? anyRaster?.href ?? null;
  return {
    tab,
    apple: apple?.href ?? android?.href ?? png?.href ?? tab,
    android: android?.href ?? apple?.href ?? png?.href ?? tab,
    title: parsed.title || hostLabel(org),
    appName: parsed.appName || manifest?.short_name || manifest?.name || hostLabel(org),
    themeColor: parsed.themeColor || "#0f172a",
    host: hostLabel(org),
  };
}

function recommendedHead(parsed: ParsedHead): string {
  return [
    '<link rel="icon" href="/favicon.ico" sizes="any">',
    '<link rel="icon" type="image/svg+xml" href="/favicon.svg">',
    '<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">',
    '<link rel="manifest" href="/site.webmanifest">',
    `<meta name="theme-color" content="${parsed.themeColor || "#0f172a"}">`,
  ].join("\n");
}

/* ----------------------------------------------------------------- Helper */

function largest(list: IconRow[]): IconRow | null {
  let best: IconRow | null = null;
  for (const i of list) if (!best || i.w * i.h > best.w * best.h) best = i;
  return best;
}

function normalize(u: string): string | null {
  if (!u) return null;
  let s = u;
  if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(s)) s = "https://" + s;
  try { const url = new URL(s); return url.hostname ? s : null; } catch { return null; }
}

function origin(u: string): string {
  const url = new URL(u);
  return `${url.protocol}//${url.host}`;
}

function hostLabel(u: string): string {
  try { return new URL(u).hostname.replace(/^www\./, ""); } catch { return u; }
}

function absolutize(rel: string | undefined, base: string): string | null {
  if (rel == null) return null;
  const r = rel.trim();
  if (!r || r.toLowerCase().startsWith("javascript:")) return null;
  if (r.toLowerCase().startsWith("data:")) return r;
  try { return new URL(r, base).toString(); } catch { return null; }
}

async function safeText(res: Response): Promise<string> {
  try { return await res.text(); } catch { return ""; }
}

type FetchResult = { url: string; status: number; res: Response };

async function fetchFollow(startUrl: string, opts: { maxRedirects?: number; timeoutMs?: number; bin?: boolean } = {}): Promise<FetchResult | null> {
  const { maxRedirects = 4, timeoutMs = 8000, bin = false } = opts;
  let cur = startUrl;
  for (let i = 0; i <= maxRedirects; i++) {
    if (!(await isPublicHttpUrl(cur))) return null;
    let res: Response;
    try {
      res = await fetch(cur, {
        redirect: "manual",
        signal: AbortSignal.timeout(timeoutMs),
        headers: { "User-Agent": UA, Accept: bin ? "image/*,*/*" : "text/html,application/xhtml+xml" },
      });
    } catch { return null; }
    const st = res.status;
    if (st >= 300 && st < 400) {
      const loc = res.headers.get("location");
      if (!loc) return null;
      let next: string;
      try { next = new URL(loc, cur).toString(); } catch { return null; }
      cur = next;
      continue;
    }
    return { url: cur, status: st, res };
  }
  return null;
}

/* ------------------------------------------------------------------- SSRF */

async function isPublicHttpUrl(u: string): Promise<boolean> {
  let url: URL;
  try { url = new URL(u); } catch { return false; }
  if (url.protocol !== "http:" && url.protocol !== "https:") return false;
  const host = url.hostname.replace(/^\[|\]$/g, "");
  if (host === "localhost" || host.endsWith(".localhost")) return false;
  if (net.isIP(host)) return !isPrivateIp(host);
  try {
    const addrs = await dns.lookup(host, { all: true });
    if (!addrs.length) return false;
    return addrs.every((a) => !isPrivateIp(a.address));
  } catch { return false; }
}

function isPrivateIp(ip: string): boolean {
  const v = net.isIP(ip);
  if (v === 4) {
    const p = ip.split(".").map(Number);
    if (p[0] === 0 || p[0] === 10 || p[0] === 127) return true;
    if (p[0] === 169 && p[1] === 254) return true;
    if (p[0] === 172 && p[1] >= 16 && p[1] <= 31) return true;
    if (p[0] === 192 && p[1] === 168) return true;
    if (p[0] === 100 && p[1] >= 64 && p[1] <= 127) return true; // CGNAT
    if (p[0] >= 224) return true; // multicast/reserved
    return false;
  }
  if (v === 6) {
    const s = ip.toLowerCase();
    if (s === "::1" || s === "::") return true;
    if (s.startsWith("fe80")) return true;
    if (s.startsWith("fc") || s.startsWith("fd")) return true;
    if (s.startsWith("::ffff:")) return isPrivateIp(s.slice(7));
    return false;
  }
  return true;
}

function decodeEntities(s: string): string {
  return s
    .replace(/&#x([0-9a-fA-F]+);/g, (_, h) => safeCodePoint(parseInt(h, 16)))
    .replace(/&#(\d+);/g, (_, d) => safeCodePoint(parseInt(d, 10)))
    .replace(/&quot;/g, '"').replace(/&apos;/g, "'")
    .replace(/&lt;/g, "<").replace(/&gt;/g, ">")
    .replace(/&amp;/g, "&"); // zuletzt, sonst Doppeldekodierung (&amp;lt; -> &lt;)
}

function safeCodePoint(n: number): string {
  try { return n > 0 && n <= 0x10ffff ? String.fromCodePoint(n) : ""; } catch { return ""; }
}
