// src/lib/spec/audit/checks.ts — the pure check registry. Each entry maps a
// `SpecItem.id` to a function that inspects a SiteSnapshot and returns a
// pass/fail/warn/na verdict with a short human (FR) detail. No DOM lib — HTML is
// parsed with small regex/string helpers (head extraction, meta/link tags).
// Items of CHECKLIST with no entry here stay "manual" (judgement checks).
import type { SiteSnapshot } from "./snapshot";

export type CheckStatus = "pass" | "fail" | "warn" | "na";

export type CheckResult = { status: CheckStatus; detail: string };

export type CheckContext = { targetCountries: string[] };

type CheckFn = (s: SiteSnapshot, ctx: CheckContext) => CheckResult;

// ── HTML helpers ────────────────────────────────────────────────────────────

/** Extract the `<head>…</head>` content (case-insensitive); falls back to the
 *  whole document when no head is found. */
function head(html: string): string {
  const m = /<head[\s>][\s\S]*?<\/head>/i.exec(html);
  return m ? m[0] : html;
}

/** All `<meta …>` tags in a chunk of HTML. */
function metaTags(html: string): string[] {
  return html.match(/<meta\b[^>]*>/gi) ?? [];
}

/** All `<link …>` tags in a chunk of HTML. */
function linkTags(html: string): string[] {
  return html.match(/<link\b[^>]*>/gi) ?? [];
}

/** Read an attribute value from a single tag string (quoted or bare). */
function attr(tag: string, name: string): string | null {
  const re = new RegExp(
    `\\b${name}\\s*=\\s*("([^"]*)"|'([^']*)'|([^\\s>]+))`,
    "i",
  );
  const m = re.exec(tag);
  if (!m) return null;
  return m[2] ?? m[3] ?? m[4] ?? "";
}

/** First meta tag whose `name`/`property`/`http-equiv` equals `key`. */
function metaBy(
  tags: string[],
  kind: "name" | "property" | "http-equiv",
  key: string,
): string | null {
  const lower = key.toLowerCase();
  for (const tag of tags) {
    if (attr(tag, kind)?.toLowerCase() === lower) return tag;
  }
  return null;
}

/** First link tag whose rel list contains `rel`. */
function linkByRel(tags: string[], rel: string): string | null {
  const lower = rel.toLowerCase();
  for (const tag of tags) {
    const rels = attr(tag, "rel")?.toLowerCase().split(/\s+/) ?? [];
    if (rels.includes(lower)) return tag;
  }
  return null;
}

/** True when the document carries at least one JSON-LD structured-data block. */
function hasJsonLd(html: string): boolean {
  return /<script\b[^>]*\btype\s*=\s*("|')?application\/ld\+json("|')?[^>]*>/i.test(
    html,
  );
}

/** Approximate visible text length inside <body> (scripts/styles/tags stripped). */
function bodyText(html: string): string {
  const m = /<body\b[^>]*>([\s\S]*?)<\/body>/i.exec(html);
  const inner = m ? m[1] : html;
  return inner
    .replace(/<script\b[\s\S]*?<\/script>/gi, " ")
    .replace(/<style\b[\s\S]*?<\/style>/gi, " ")
    .replace(/<[^>]+>/g, " ")
    .replace(/\s+/g, " ")
    .trim();
}

/** Well-known AI crawler user-agents a robots.txt may target. */
const AI_CRAWLER_UAS = [
  "GPTBot",
  "OAI-SearchBot",
  "ChatGPT-User",
  "ClaudeBot",
  "Claude-Web",
  "anthropic-ai",
  "Google-Extended",
  "PerplexityBot",
  "CCBot",
  "Applebot-Extended",
  "Bytespider",
  "Meta-ExternalAgent",
  "cohere-ai",
];

// ── Registry ────────────────────────────────────────────────────────────────

export const CHECKS: Record<string, CheckFn> = {
  // ── foundations ──────────────────────────────────────────────────────────
  "foundations/doctype": (s) => {
    if (!s.home.ok) return { status: "fail", detail: "Home inaccessible." };
    return /^\s*<!doctype html>/i.test(s.home.html)
      ? { status: "pass", detail: "Doctype HTML5 présent." }
      : { status: "fail", detail: "Pas de <!doctype html> en première ligne." };
  },

  "foundations/html-lang": (s) => {
    if (!s.home.ok) return { status: "fail", detail: "Home inaccessible." };
    const m = /<html\b[^>]*>/i.exec(s.home.html);
    const lang = m ? attr(m[0], "lang") : null;
    if (!lang)
      return { status: "fail", detail: "Attribut lang absent sur <html>." };
    return /^[a-z]{2,3}(-[a-z0-9]+)*$/i.test(lang)
      ? { status: "pass", detail: `lang="${lang}".` }
      : { status: "warn", detail: `lang="${lang}" non conforme (BCP 47).` };
  },

  "foundations/meta-charset": (s) => {
    if (!s.home.ok) return { status: "fail", detail: "Home inaccessible." };
    const tags = metaTags(head(s.home.html));
    const charsetTag = tags.find((t) => attr(t, "charset") != null);
    if (charsetTag) {
      const v = attr(charsetTag, "charset") ?? "";
      return /utf-8/i.test(v)
        ? { status: "pass", detail: `charset=${v}.` }
        : { status: "warn", detail: `charset=${v} (UTF-8 attendu).` };
    }
    const httpEquiv = metaBy(tags, "http-equiv", "content-type");
    if (httpEquiv && /utf-8/i.test(attr(httpEquiv, "content") ?? ""))
      return { status: "pass", detail: "charset UTF-8 via http-equiv." };
    return {
      status: "fail",
      detail: "Pas de <meta charset> UTF-8 dans le <head>.",
    };
  },

  "foundations/meta-viewport": (s) => {
    if (!s.home.ok) return { status: "fail", detail: "Home inaccessible." };
    const tag = metaBy(metaTags(head(s.home.html)), "name", "viewport");
    if (!tag) return { status: "fail", detail: "Pas de <meta viewport>." };
    const content = attr(tag, "content") ?? "";
    return /user-scalable\s*=\s*no/i.test(content)
      ? { status: "warn", detail: "viewport présent mais user-scalable=no." }
      : { status: "pass", detail: "viewport présent." };
  },

  "foundations/title": (s) => {
    if (!s.home.ok) return { status: "fail", detail: "Home inaccessible." };
    const titles = s.home.html.match(/<title[^>]*>([\s\S]*?)<\/title>/gi) ?? [];
    const first = titles[0];
    if (!first) return { status: "fail", detail: "Pas de <title>." };
    const text = (
      /<title[^>]*>([\s\S]*?)<\/title>/i.exec(first)?.[1] ?? ""
    ).trim();
    if (text === "") return { status: "fail", detail: "<title> vide." };
    if (titles.length > 1)
      return {
        status: "warn",
        detail: `${titles.length} <title> (1 attendu).`,
      };
    return { status: "pass", detail: `<title>: « ${text.slice(0, 60)} ».` };
  },

  "foundations/meta-description": (s) => {
    if (!s.home.ok) return { status: "fail", detail: "Home inaccessible." };
    const tag = metaBy(metaTags(head(s.home.html)), "name", "description");
    const content = tag ? (attr(tag, "content") ?? "").trim() : "";
    return content
      ? { status: "pass", detail: "Meta description présente." }
      : { status: "warn", detail: "Pas de meta description." };
  },

  "foundations/canonical-url": (s) => {
    if (!s.home.ok) return { status: "fail", detail: "Home inaccessible." };
    const tag = linkByRel(linkTags(head(s.home.html)), "canonical");
    if (!tag) return { status: "warn", detail: "Pas de link rel=canonical." };
    const href = attr(tag, "href") ?? "";
    return { status: "pass", detail: `Canonical: ${href || "(href vide)"}.` };
  },

  "foundations/open-graph": (s) => {
    if (!s.home.ok) return { status: "fail", detail: "Home inaccessible." };
    const tags = metaTags(head(s.home.html));
    const hasTitle = metaBy(tags, "property", "og:title") != null;
    const hasImage = metaBy(tags, "property", "og:image") != null;
    if (hasTitle && hasImage)
      return { status: "pass", detail: "og:title + og:image présents." };
    if (hasTitle || hasImage)
      return {
        status: "warn",
        detail: `Open Graph incomplet (${hasTitle ? "og:title" : "og:image"} seul).`,
      };
    return { status: "warn", detail: "Pas de balises Open Graph." };
  },

  "foundations/feed-discovery": (s) => {
    if (!s.home.ok) return { status: "fail", detail: "Home inaccessible." };
    const tags = linkTags(head(s.home.html)).filter((t) =>
      (attr(t, "rel")?.toLowerCase().split(/\s+/) ?? []).includes("alternate"),
    );
    const hasFeed = tags.some((t) =>
      /(rss|atom|feed\+json|application\/json)/i.test(attr(t, "type") ?? ""),
    );
    return hasFeed
      ? {
          status: "pass",
          detail: "Flux RSS/Atom/JSON découvrable (link alternate).",
        }
      : {
          status: "na",
          detail: "Pas de flux déclaré (non requis pour tout site).",
        };
  },

  "foundations/favicons": (s) => {
    if (!s.home.ok) return { status: "fail", detail: "Home inaccessible." };
    const hasIcon = linkTags(head(s.home.html)).some((t) =>
      (attr(t, "rel")?.toLowerCase().split(/\s+/) ?? []).some((r) =>
        /^(icon|shortcut|apple-touch-icon|mask-icon)$/.test(r),
      ),
    );
    return hasIcon
      ? { status: "pass", detail: "Favicon déclaré (link rel=icon)." }
      : { status: "warn", detail: "Pas de <link rel=icon> dans le <head>." };
  },

  "foundations/theme-color": (s) => {
    if (!s.home.ok) return { status: "fail", detail: "Home inaccessible." };
    const tag = metaBy(metaTags(head(s.home.html)), "name", "theme-color");
    return tag
      ? { status: "pass", detail: "<meta name=theme-color> présent." }
      : { status: "na", detail: "Pas de meta theme-color (optionnel)." };
  },

  // ── seo ──────────────────────────────────────────────────────────────────
  "seo/meta-robots": (s) => {
    if (!s.home.ok) return { status: "fail", detail: "Home inaccessible." };
    const tag = metaBy(metaTags(head(s.home.html)), "name", "robots");
    const xRobots = s.home.headers["x-robots-tag"] ?? "";
    const policy =
      `${tag ? (attr(tag, "content") ?? "") : ""} ${xRobots}`.toLowerCase();
    if (/\bnoindex\b/.test(policy))
      return { status: "fail", detail: "Home en noindex." };
    return tag || xRobots
      ? {
          status: "pass",
          detail: `Policy robots: ${policy.trim() || "(vide)"}.`,
        }
      : { status: "pass", detail: "Indexable (pas de noindex)." };
  },

  "seo/robots-txt": (s) => {
    if (
      s.robotsTxt.ok &&
      /(user-agent|disallow|sitemap)/i.test(s.robotsTxt.text)
    )
      return { status: "pass", detail: "robots.txt servi et valide." };
    if (s.robotsTxt.ok)
      return {
        status: "warn",
        detail: "robots.txt servi mais vide/non parsable.",
      };
    return {
      status: "fail",
      detail: `robots.txt absent (${s.robotsTxt.status}).`,
    };
  },

  "seo/xml-sitemaps": (s) => {
    if (s.sitemapXml.ok && /<(urlset|sitemapindex)/i.test(s.sitemapXml.text))
      return { status: "pass", detail: "sitemap.xml servi." };
    if (/sitemap\s*:/i.test(s.robotsTxt.text))
      return { status: "pass", detail: "Sitemap référencé dans robots.txt." };
    return {
      status: "fail",
      detail: "Pas de sitemap.xml ni de référence robots.txt.",
    };
  },

  "seo/structured-data": (s) => {
    if (!s.home.ok) return { status: "fail", detail: "Home inaccessible." };
    return hasJsonLd(s.home.html)
      ? { status: "pass", detail: "Données structurées JSON-LD présentes." }
      : { status: "warn", detail: "Pas de <script type=application/ld+json>." };
  },

  "seo/heading-hierarchy": (s) => {
    if (!s.home.ok) return { status: "fail", detail: "Home inaccessible." };
    const count = (s.home.html.match(/<h1\b[^>]*>/gi) ?? []).length;
    if (count === 1) return { status: "pass", detail: "Un seul <h1>." };
    if (count === 0)
      return { status: "fail", detail: "Aucun <h1> sur la home." };
    return { status: "warn", detail: `${count} <h1> (un seul recommandé).` };
  },

  "seo/server-side-rendering": (s) => {
    if (!s.home.ok) return { status: "fail", detail: "Home inaccessible." };
    const text = bodyText(s.home.html);
    const hasMain = /<main\b/i.test(s.home.html);
    const hasHeading = /<h[1-6]\b/i.test(s.home.html);
    if (text.length >= 500 || (hasMain && hasHeading && text.length >= 120))
      return {
        status: "pass",
        detail: `Contenu rendu côté serveur (~${text.length} car. dans <body>).`,
      };
    return {
      status: "warn",
      detail: `<body> quasi vide (~${text.length} car.) : shell SPA probable.`,
    };
  },

  // ── security ─────────────────────────────────────────────────────────────
  "security/https-tls": (s) => {
    const url = s.home.finalUrl || s.baseUrl;
    return url.startsWith("https://")
      ? { status: "pass", detail: "Servi en HTTPS." }
      : { status: "fail", detail: `URL finale non HTTPS (${url}).` };
  },

  "security/hsts": (s) => {
    const v = s.home.headers["strict-transport-security"];
    if (!v) return { status: "warn", detail: "Pas d'en-tête HSTS." };
    const max = /max-age=(\d+)/i.exec(v)?.[1];
    return { status: "pass", detail: `HSTS présent (max-age=${max ?? "?"}).` };
  },

  "security/content-security-policy": (s) => {
    const v = s.home.headers["content-security-policy"];
    return v
      ? { status: "pass", detail: "Content-Security-Policy présente." }
      : { status: "warn", detail: "Pas de Content-Security-Policy." };
  },

  "security/x-content-type-options": (s) => {
    const v = (s.home.headers["x-content-type-options"] ?? "").toLowerCase();
    return v.includes("nosniff")
      ? { status: "pass", detail: "X-Content-Type-Options: nosniff." }
      : { status: "warn", detail: "Pas de X-Content-Type-Options: nosniff." };
  },

  "security/referrer-policy": (s) => {
    const v = s.home.headers["referrer-policy"];
    return v
      ? { status: "pass", detail: `Referrer-Policy: ${v}.` }
      : { status: "warn", detail: "Pas d'en-tête Referrer-Policy." };
  },

  "security/frame-ancestors": (s) => {
    const xfo = s.home.headers["x-frame-options"];
    const csp = s.home.headers["content-security-policy"] ?? "";
    if (xfo) return { status: "pass", detail: `X-Frame-Options: ${xfo}.` };
    if (/frame-ancestors/i.test(csp))
      return { status: "pass", detail: "CSP frame-ancestors présent." };
    return {
      status: "warn",
      detail: "Pas de X-Frame-Options ni CSP frame-ancestors.",
    };
  },

  "security/security-txt": (s) => {
    if (s.securityTxt.ok && /contact\s*:/i.test(s.securityTxt.text))
      return { status: "pass", detail: "/.well-known/security.txt présent." };
    if (s.securityTxt.ok)
      return {
        status: "warn",
        detail: "security.txt servi mais sans Contact.",
      };
    return { status: "warn", detail: "Pas de /.well-known/security.txt." };
  },

  "security/mixed-content": (s) => {
    const url = s.home.finalUrl || s.baseUrl;
    if (!url.startsWith("https://"))
      return {
        status: "na",
        detail: "Home non HTTPS : mixed content sans objet.",
      };
    if (!s.home.ok) return { status: "fail", detail: "Home inaccessible." };
    // Sous-ressources chargées en clair (src/href http://) sur une page https.
    const insecure =
      s.home.html.match(/\b(?:src|href)\s*=\s*["']http:\/\/[^"']+["']/gi) ?? [];
    return insecure.length === 0
      ? { status: "pass", detail: "Aucune sous-ressource en http:// détectée." }
      : {
          status: "fail",
          detail: `${insecure.length} référence(s) http:// sur une page HTTPS.`,
        };
  },

  "security/permissions-policy": (s) => {
    const v = s.home.headers["permissions-policy"];
    return v
      ? { status: "pass", detail: "En-tête Permissions-Policy présent." }
      : { status: "warn", detail: "Pas d'en-tête Permissions-Policy." };
  },

  "security/cookie-attributes": (s) => {
    const v = s.home.headers["set-cookie"];
    if (!v) return { status: "na", detail: "Aucun cookie posé sur la home." };
    const lower = v.toLowerCase();
    const flags = {
      secure: lower.includes("secure"),
      httpOnly: lower.includes("httponly"),
      sameSite: lower.includes("samesite"),
    };
    const missing = Object.entries(flags)
      .filter(([, ok]) => !ok)
      .map(([k]) => k);
    return missing.length === 0
      ? { status: "pass", detail: "Cookies : Secure + HttpOnly + SameSite." }
      : { status: "warn", detail: `Cookies sans : ${missing.join(", ")}.` };
  },

  "security/cross-origin-isolation": (s) => {
    const names = [
      "cross-origin-opener-policy",
      "cross-origin-embedder-policy",
      "cross-origin-resource-policy",
    ];
    const present = names.filter((n) => s.home.headers[n]);
    if (present.length === names.length)
      return { status: "pass", detail: "COOP + COEP + CORP présents." };
    if (present.length === 0)
      return { status: "warn", detail: "Aucun en-tête COOP/COEP/CORP." };
    const missing = names.filter((n) => !s.home.headers[n]);
    return {
      status: "warn",
      detail: `Isolation partielle — manquants : ${missing.join(", ")}.`,
    };
  },

  "security/reporting-endpoints": (s) => {
    if (s.home.headers["reporting-endpoints"])
      return { status: "pass", detail: "En-tête Reporting-Endpoints présent." };
    if (s.home.headers["report-to"])
      return {
        status: "warn",
        detail: "Report-To legacy seul (Reporting-Endpoints attendu).",
      };
    return { status: "warn", detail: "Pas d'en-tête Reporting-Endpoints." };
  },

  "security/trusted-types": (s) => {
    const csp = s.home.headers["content-security-policy"] ?? "";
    if (/require-trusted-types-for/i.test(csp))
      return {
        status: "pass",
        detail: "CSP avec require-trusted-types-for.",
      };
    if (csp)
      return {
        status: "warn",
        detail: "CSP sans require-trusted-types-for (Trusted Types inactifs).",
      };
    return { status: "warn", detail: "Pas de CSP (Trusted Types inactifs)." };
  },

  // ── performance ──────────────────────────────────────────────────────────
  "performance/compression": (s) => {
    // fetch décompresse automatiquement et retire souvent content-encoding ;
    // l'absence n'est donc pas une preuve de non-compression → warn, pas fail.
    const v = (s.home.headers["content-encoding"] ?? "").toLowerCase();
    if (/\b(br|gzip|zstd|deflate)\b/.test(v))
      return { status: "pass", detail: `Compression: ${v}.` };
    return {
      status: "warn",
      detail:
        "content-encoding absent (souvent masqué par la décompression fetch).",
    };
  },

  "performance/cache-control": (s) => {
    const v = s.home.headers["cache-control"];
    return v
      ? { status: "pass", detail: `Cache-Control: ${v}.` }
      : { status: "warn", detail: "Pas d'en-tête Cache-Control sur la home." };
  },

  "performance/conditional-requests": (s) => {
    const etag = s.home.headers.etag;
    const lastMod = s.home.headers["last-modified"];
    if (etag)
      return { status: "pass", detail: `ETag présent (${etag.slice(0, 32)}).` };
    if (lastMod) return { status: "pass", detail: `Last-Modified présent.` };
    return { status: "warn", detail: "Ni ETag ni Last-Modified (pas de 304)." };
  },

  "performance/http3": (s) => {
    const v = (s.home.headers["alt-svc"] ?? "").toLowerCase();
    return /\bh3(-\d+)?=/.test(v)
      ? { status: "pass", detail: "HTTP/3 annoncé via Alt-Svc." }
      : { status: "na", detail: "Pas d'Alt-Svc h3 (HTTP/3 non annoncé)." };
  },

  // Topic `optional` : l'absence n'est pas pénalisée (miroir http3/theme-color).
  "performance/server-timing": (s) => {
    const v = s.home.headers["server-timing"];
    return v
      ? { status: "pass", detail: `Server-Timing : ${v.slice(0, 60)}.` }
      : { status: "na", detail: "Pas de Server-Timing (optionnel)." };
  },

  // ── agent-readiness ──────────────────────────────────────────────────────
  "agent-readiness/robots-for-ai-crawlers": (s) => {
    if (!s.robotsTxt.ok)
      return { status: "na", detail: "Pas de robots.txt à inspecter." };
    const found = AI_CRAWLER_UAS.filter((ua) =>
      new RegExp(`user-agent\\s*:\\s*${ua}\\b`, "i").test(s.robotsTxt.text),
    );
    return found.length > 0
      ? {
          status: "pass",
          detail: `robots.txt cible des UA IA: ${found.join(", ")}.`,
        }
      : {
          status: "na",
          detail: "robots.txt ne mentionne aucun UA de crawler IA connu.",
        };
  },

  "agent-readiness/structured-data-for-agents": (s) => {
    if (!s.home.ok) return { status: "fail", detail: "Home inaccessible." };
    return hasJsonLd(s.home.html)
      ? {
          status: "pass",
          detail: "JSON-LD exploitable par les agents présent.",
        }
      : { status: "warn", detail: "Pas de JSON-LD pour les agents." };
  },

  "agent-readiness/llms-txt": (s) => {
    if (s.llmsTxt.ok && s.llmsTxt.text.trim().length > 0)
      return { status: "pass", detail: "/llms.txt présent." };
    return { status: "warn", detail: "Pas de /llms.txt." };
  },

  // ── i18n ─────────────────────────────────────────────────────────────────
  "i18n/hreflang": (s, ctx) => {
    const multi = (ctx.targetCountries?.length ?? 0) > 1;
    if (!multi)
      return { status: "na", detail: "Mono-locale : hreflang non requis." };
    if (!s.home.ok) return { status: "fail", detail: "Home inaccessible." };
    const tags = linkTags(head(s.home.html)).filter((t) =>
      (attr(t, "rel")?.toLowerCase().split(/\s+/) ?? []).includes("alternate"),
    );
    const hasHreflang = tags.some((t) => attr(t, "hreflang") != null);
    return hasHreflang
      ? { status: "pass", detail: "Balises hreflang présentes." }
      : {
          status: "fail",
          detail: "Multi-locale sans link rel=alternate hreflang.",
        };
  },
};
