// src/lib/keywords/prompt/build-prompt.ts
// Pure, deterministic builder turning a KeywordPromptBundle into a paste-ready
// FRENCH prompt. No I/O, no DB, no React. See types.ts for the shared contract.

import type { SerpCompetitor } from "@/lib/keywords/serp-types";
import type { KeywordBrief } from "@/lib/keywords/ytg-types";
import type {
  GapTerm,
  GuideTerm,
  NgramAggregate,
  TermRange,
} from "@/lib/semantic/types";
import type {
  BuildPromptOptions,
  KeywordPromptBundle,
  PromptTarget,
  PromptTask,
} from "./types";

const DASH = "—";

function roleLine(task: PromptTask): string {
  switch (task) {
    case "brief":
      return "Tu es un stratège éditorial SEO senior.";
    case "redaction":
      return "Tu es un rédacteur SEO expert francophone.";
    case "optim":
      return "Tu es un consultant SEO on-page senior.";
  }
}

function taskLine(task: PromptTask, target: PromptTarget): string {
  let line: string;
  switch (task) {
    case "brief":
      line =
        "Rédige un BRIEF ÉDITORIAL SEO complet et actionnable pour le mot-clé ci-dessous.";
      break;
    case "redaction":
      line = "RÉDIGE L'ARTICLE complet, optimisé SEO, prêt à publier.";
      break;
    case "optim":
      line =
        "AUDITE puis OPTIMISE une page existante (gap de contenu, sémantique, maillage).";
      break;
  }
  if (target === "claude-code") {
    line += " Tu écris la page directement dans le projet.";
  }
  return line;
}

function roleSection(opts: BuildPromptOptions): string {
  return ["# RÔLE", roleLine(opts.task), taskLine(opts.task, opts.target)].join(
    "\n",
  );
}

function targetSection(bundle: KeywordPromptBundle): string {
  const lines = [
    "# MOT-CLÉ CIBLE",
    `Terme : ${bundle.term}`,
    `Marché : ${bundle.country}`,
  ];
  if (bundle.intent.length) {
    lines.push(`Intention : ${bundle.intent.join(", ")}`);
  }
  return lines.join("\n");
}

function metricsSection(bundle: KeywordPromptBundle): string {
  const lines = ["# MÉTRIQUES"];
  if (bundle.vol == null) {
    lines.push("Métriques: non disponibles");
    return lines.join("\n");
  }
  const kd = bundle.kd == null ? DASH : String(bundle.kd);
  const cpc = bundle.cpc == null ? DASH : String(bundle.cpc);
  lines.push(`Volume: ${bundle.vol}/mois · KD: ${kd}/100 · CPC: $${cpc}`);
  lines.push(`(source: ${bundle.primarySource})`);
  return lines.join("\n");
}

function num(value: number | null): string {
  return value == null ? DASH : String(value);
}

function serpTable(competitors: SerpCompetitor[]): string {
  const header = ["#", "domaine", "backlinks", "ref.dom", "AS"];
  const rows = competitors.map((c) => [
    String(c.position),
    c.domain,
    num(c.backlinks),
    num(c.refDomains),
    num(c.pageAuthority),
  ]);
  const widths = header.map((h, i) =>
    Math.max(h.length, ...rows.map((r) => r[i].length)),
  );
  const fmt = (cells: string[]) =>
    cells
      .map((cell, i) => cell.padEnd(widths[i]))
      .join("  ")
      .trimEnd();
  return [fmt(header), ...rows.map(fmt)].join("\n");
}

function serpSection(bundle: KeywordPromptBundle): string {
  const lines = ["# SERP — TOP CONCURRENTS"];
  if (!bundle.competitors.length) {
    lines.push("SERP : non disponible — à récupérer.");
    return lines.join("\n");
  }
  lines.push(serpTable(bundle.competitors));
  const crawled = bundle.competitors.filter(
    (c) => c.title || c.metaDescription,
  );
  if (crawled.length) {
    lines.push("");
    for (const c of crawled) {
      lines.push(
        `${c.position}. ${c.domain} — ${c.title ?? c.metaDescription}`,
      );
    }
  }
  return lines.join("\n");
}

function capJoin(values: string[], cap: number): string {
  return values.slice(0, cap).join(", ");
}

function ytgSection(bundle: KeywordPromptBundle): string {
  const lines = ["# GRAMMAGES & TERMES (YourTextGuru)"];
  const brief: KeywordBrief | null = bundle.brief;
  if (!brief) {
    lines.push(
      "Brief YTG : non disponible — propose un plan Hn et les entités à couvrir.",
    );
    return lines.join("\n");
  }
  if (brief.topTerms.length) {
    lines.push(`Termes à couvrir (top): ${capJoin(brief.topTerms, 30)}`);
  }
  if (brief.entities.length) {
    lines.push(`Entités: ${capJoin(brief.entities, 20)}`);
  }
  if (brief.twoGrams.length) {
    lines.push(`Bigrammes clés: ${capJoin(brief.twoGrams, 12)}`);
  }
  if (brief.questions.length) {
    lines.push(`Questions (PAA): ${brief.questions.join(", ")}`);
  }
  if (brief.objectives.length) {
    lines.push(`Objectifs: ${brief.objectives.join(", ")}`);
  }
  return lines.join("\n");
}

// ── GUIDE SÉMANTIQUE (profil pondéré de la SERP, S3 pipeline) ──────────────

/** Troncatures du guide — codées en constantes nommées (règle du brief S4/Task 4).
 *  Calibrage YTG (Task 4, fiabilité serp-semantics) : caps relevés et
 *  n-grammes scindés en deux listes (bigrammes/trigrammes) au lieu d'une
 *  liste mixte. */
const GUIDE_TERMS_CAP = 20;
const GUIDE_BIGRAMS_CAP = 15;
const GUIDE_TRIGRAMS_CAP = 10;
const GUIDE_QUESTIONS_CAP = 8;
const GAP_TERMS_CAP = 10;

const ZONE_LABELS: Record<string, string> = {
  title: "Title",
  h1: "H1",
  h2h3: "H2/H3",
  slug: "slug",
  alt: "attributs alt",
  anchor: "ancres",
  meta: "meta description",
  body: "corps de texte",
};

function zoneLabel(zone: string): string {
  return ZONE_LABELS[zone] ?? zone;
}

const INTENT_MODE_LABELS: Record<string, string> = {
  inferred: "inférée",
  manual: "manuelle",
};

function intentModeLabel(mode: string): string {
  return INTENT_MODE_LABELS[mode] ?? mode;
}

/** French thousands grouping; `toLocaleString` inserts a narrow no-break
 *  space (U+202F) which we fold to a plain space (brief: "espace insécable
 *  fine acceptée en espace simple"). */
function formatFR(value: number): string {
  return value.toLocaleString("fr-FR").replace(/[\u00A0\u202F]/g, " ");
}

function formatScore(value: number): string {
  return value.toFixed(2);
}

function pluralize(count: number, singular: string, plural: string): string {
  return count === 1 ? singular : plural;
}

/** `{0,0,0}` = terme absent du Top 3 (cf. JSDoc `TermRange`) — pas de
 *  fourchette à afficher dans ce cas. */
function termRangeLabel(range: TermRange): string {
  if (range.min === 0 && range.median === 0 && range.max === 0) {
    return "présent hors top 3";
  }
  return `${range.min}–${range.max} occurrences`;
}

function guideTermLine(term: GuideTerm, score: number): string {
  return `- « ${term.display} » : ${termRangeLabel(term.top3)} (zone clé : ${zoneLabel(
    term.recommendedZone,
  )}) — score ${formatScore(score)}`;
}

/** Rendu YTG-style d'une liste de n-grammes (bigrammes OU trigrammes,
 *  jamais mixés — calibrage Task 4). Retourne `null` quand la liste cappée
 *  est vide, pour que l'appelant omette la ligne plutôt que d'afficher un
 *  libellé sans expression. */
function ngramListLine(label: string, ngrams: NgramAggregate[]): string | null {
  if (!ngrams.length) {
    return null;
  }
  return `${label} : ${ngrams.map((n) => `« ${n.text} »`).join(", ")}`;
}

function gapTermLine(gapTerm: GapTerm, display: string): string {
  return `- « ${display} » : attendu ${formatScore(
    gapTerm.expectedScore,
  )} vs client ${formatScore(gapTerm.clientScore)} (écart ${formatScore(
    gapTerm.delta,
  )})`;
}

/** Returns `null` when `bundle.semanticProfile` is absent — no S3 analysis
 *  has run yet for this keyword/country, so the section is simply omitted
 *  (same optional pattern as `competitorDetailSection`). */
function semanticGuideSection(bundle: KeywordPromptBundle): string | null {
  const profile = bundle.semanticProfile;
  if (!profile) {
    return null;
  }
  const { intent, coverage, guide } = profile;

  const lines = [
    "# GUIDE SÉMANTIQUE (profil pondéré de la SERP)",
    `Intention détectée : ${intent.curve} (${intentModeLabel(intent.mode)}, ${
      coverage.analyzed
    }/${coverage.total} concurrents analysés)`,
    "Termes à employer (fourchettes d'occurrences observées dans le Top 3) :",
  ];

  for (const term of guide.terms.slice(0, GUIDE_TERMS_CAP)) {
    const score = profile.terms.find((t) => t.term === term.term)?.score ?? 0;
    lines.push(guideTermLine(term, score));
  }

  // Deux listes séparées bigrammes/trigrammes (calibrage YTG, Task 4) — le
  // crate sépare déjà `ngrams.bigrams`/`ngrams.trigrams` au niveau profil ;
  // le `guide.ngrams` (flat, combiné) n'est plus utilisé ici.
  const bigramsLine = ngramListLine(
    "Expressions (2 mots)",
    profile.ngrams.bigrams.slice(0, GUIDE_BIGRAMS_CAP),
  );
  if (bigramsLine) {
    lines.push(bigramsLine);
  }
  const trigramsLine = ngramListLine(
    "Expressions (3 mots)",
    profile.ngrams.trigrams.slice(0, GUIDE_TRIGRAMS_CAP),
  );
  if (trigramsLine) {
    lines.push(trigramsLine);
  }

  const questions = guide.questions.slice(0, GUIDE_QUESTIONS_CAP);
  if (questions.length) {
    lines.push(
      `Questions à traiter (issues des Hn concurrents) : ${questions.join(", ")}`,
    );
  }

  const obj = guide.objectives;
  lines.push(
    `Objectifs structurels : ~${formatFR(obj.wordCount.median)} mots (médiane SERP), ${
      obj.h2Count
    } H2, ${obj.lists} ${pluralize(obj.lists, "liste", "listes")}, ${
      obj.tables
    } ${pluralize(obj.tables, "tableau", "tableaux")}`,
  );

  if (profile.gap) {
    lines.push("", "Ajustements (gap analysis) :");
    for (const gapTerm of profile.gap.terms.slice(0, GAP_TERMS_CAP)) {
      const display =
        profile.terms.find((t) => t.term === gapTerm.term)?.display ??
        gapTerm.term;
      lines.push(gapTermLine(gapTerm, display));
    }
  }

  return lines.join("\n");
}

/** A competitor carries real crawled content (word count, outline or markdown). */
function hasContent(c: SerpCompetitor): boolean {
  return (
    c.wordCount != null ||
    (c.hnOutline != null && c.hnOutline.length > 0) ||
    (c.markdown != null && c.markdown.trim().length > 0)
  );
}

const EXCERPT_CHARS = 300;

function outlineLines(outline: SerpCompetitor["hnOutline"]): string[] {
  if (!outline?.length) {
    return [];
  }
  return outline.map((node) => {
    const prefix = "#".repeat(node.level);
    return `   ${prefix} ${node.text}`;
  });
}

function excerptLines(markdown: SerpCompetitor["markdown"]): string[] {
  if (markdown == null) {
    return [];
  }
  const text = markdown.trim().replace(/\s+/g, " ");
  if (!text) {
    return [];
  }
  const truncated = text.length > EXCERPT_CHARS;
  const snippet = truncated ? `${text.slice(0, EXCERPT_CHARS)}…` : text;
  return [`   Extrait : ${snippet}`];
}

function competitorDetail(c: SerpCompetitor): string[] {
  const header =
    c.wordCount == null
      ? `${c.position}. ${c.domain}`
      : `${c.position}. ${c.domain} ${DASH} ${c.wordCount} mots`;
  return [header, ...outlineLines(c.hnOutline), ...excerptLines(c.markdown)];
}

function competitorDetailSection(bundle: KeywordPromptBundle): string | null {
  const withContent = bundle.competitors.filter(hasContent);
  if (!withContent.length) {
    return null;
  }
  const lines = ["# CONTENU CONCURRENTS (extrait du crawl)"];
  for (const c of withContent) {
    lines.push("", ...competitorDetail(c));
  }
  return lines.join("\n");
}

function competitiveSection(
  bundle: KeywordPromptBundle,
  target: PromptTarget,
): string {
  const lines = ["# ANALYSE CONCURRENTIELLE"];
  const uncrawled = bundle.competitors.filter((c) => !hasContent(c));
  const someCrawled = bundle.competitors.some(hasContent);

  if (someCrawled) {
    lines.push(
      "Le contenu réel des concurrents crawlés est fourni ci-dessous (plan Hn, nombre de mots, extrait). Appuie-toi dessus pour déduire le content gap (ce que tous couvrent vs ce qui manque).",
    );
  }

  if (uncrawled.length) {
    lines.push(
      "Pour chaque URL du top SERP non encore crawlée : scrape la page, extrais le plan Hn complet, les entités et le nombre de mots, puis complète le content gap.",
    );
    if (target === "claude-code") {
      lines.push("Utilise les outils MCP listés ci-dessous + `fetch(url)`.");
    } else {
      lines.push("Travaille à partir des concurrents listés ci-dessus.");
    }
  }

  return lines.join("\n");
}

function toolsSection(bundle: KeywordPromptBundle): string {
  return [
    "# OUTILS (MCP)",
    `- seo-insight.get_keyword("${bundle.term}") → métriques + SERP`,
    `- seo-insight.get_keyword_brief("${bundle.term}") → brief éditorial (grammages, entités)`,
    "- fetch(url) → scrape le contenu des concurrents",
  ].join("\n");
}

function deliverableSection(task: PromptTask): string {
  const lines = ["# LIVRABLE ATTENDU"];
  switch (task) {
    case "brief":
      lines.push(
        "- Title + meta description (≤155 caractères)",
        "- H1 + plan Hn complet",
        "- Entités à couvrir + questions PAA",
        "- Longueur cible argumentée",
      );
      break;
    case "redaction":
      lines.push(
        "- Article Markdown complet respectant le plan Hn",
        "- Couvre le content gap identifié",
        "- FAQ construite depuis les PAA",
        "- Maillage interne pertinent",
      );
      break;
    case "optim":
      lines.push(
        "- Correctifs on-page priorisés (impact × effort)",
        "- Sections à ajouter / réécrire",
        "- Recommandations title / Hn / entités / maillage",
      );
      break;
  }
  return lines.join("\n");
}

function footer(target: PromptTarget): string {
  return `// Généré par SEO Insight · cible ${target} · données du mot-clé ci-dessus.`;
}

export function buildKeywordPrompt(
  bundle: KeywordPromptBundle,
  opts: BuildPromptOptions,
): string {
  const sections: string[] = [
    roleSection(opts),
    targetSection(bundle),
    metricsSection(bundle),
    serpSection(bundle),
    ytgSection(bundle),
  ];
  const guide = semanticGuideSection(bundle);
  if (guide) {
    sections.push(guide);
  }
  sections.push(competitiveSection(bundle, opts.target));
  const detail = competitorDetailSection(bundle);
  if (detail) {
    sections.push(detail);
  }
  if (opts.target === "claude-code") {
    sections.push(toolsSection(bundle));
  }
  sections.push(deliverableSection(opts.task));
  sections.push(footer(opts.target));
  return sections.join("\n\n");
}
