// src/lib/mangools/list-import.ts — transformation PURE d'une liste KWFinder en
// lignes `keywords` prêtes à insérer. Aucune I/O : ni `db`, ni `server-only`, ni
// réseau — c'est ce qui rend les règles de pays et de rejet testables sans base,
// comme `gsc-step-logic.ts` / `keywords-step-logic.ts` côté onboarding. L'écriture
// (quota, verrou d'org, clause de conflit) vit dans `list-actions.ts`.
import * as wasm from "import-core";
import type { keywords } from "@/lib/db/domain-schema";
import { keywordId } from "@/lib/keywords/import";
import type { KeywordStage, KeywordTheme } from "@/lib/keywords/types";
import { metricsFromMangools, trendFromMangools } from "./normalize";
import type { MangoolsKeywordItem, MangoolsListDetail } from "./types";

/** Borne du terme, miroir de `addRelatedKeywordsAction` : au-delà, la ligne est
 *  écartée (payload abusif ou donnée corrompue). */
const MAX_TERM_LENGTH = 512;

/** Ligne `keywords` prête à insérer. Le schéma n'est importé QUE comme type :
 *  l'import est effacé à la compilation, le module reste sans dépendance runtime
 *  sur la couche base. */
export type ListKeywordRow = typeof keywords.$inferInsert;

/**
 * Ce que la localisation d'une liste autorise à faire, sans jamais choisir à la
 * place de l'utilisateur.
 *
 * - `ok` — un seul pays, présent dans les cibles du site : on importe.
 * - `off-target` — un seul pays, ABSENT des cibles du site : on importe quand
 *   même (la liste est cohérente, c'est le site qui ne la couvre pas), mais
 *   l'écart est remonté pour être affiché AVANT l'import.
 * - `mixed` — plusieurs pays dans la même liste : rien n'est importé. Choisir
 *   l'un des deux serait exactement le repli arbitraire que ce module existe
 *   pour éviter.
 * - `unknown` — aucun code pays lisible : idem, on ne devine pas.
 */
export type CountryVerdict =
  | { kind: "ok"; country: string }
  | { kind: "mixed"; countries: string[] }
  | { kind: "off-target"; country: string; siteCountries: string[] }
  | { kind: "unknown" };

/** Codes pays alpha-2 : trim, MAJUSCULES, vides écartés, dédupliqués, triés.
 *  Le tri rend le verdict `mixed` stable quel que soit l'ordre des mots-clés. */
function normalizeCountries(codes: readonly string[]): string[] {
  return [
    ...new Set(
      codes
        .map((c) => (typeof c === "string" ? c.trim().toUpperCase() : ""))
        .filter((c) => c !== ""),
    ),
  ].sort();
}

/**
 * Le pays d'import vient des mots-clés de la liste, jamais d'un repli sur le
 * premier pays cible du site.
 *
 * Le chemin voisin (`related-actions.ts`) code en dur
 * `siteRow.targetCountries?.[0] || "US"` : c'est le couple « langue fr » + «
 * pays US » que ce dépôt a déjà corrigé deux fois, et il ignore la localisation
 * que Mangools renvoie pourtant en clair. Ici, un écart se dit ; il ne se
 * comble pas en silence.
 */
export function resolveListCountry(
  listCountries: string[],
  siteCountries: string[],
): CountryVerdict {
  const listed = normalizeCountries(listCountries);
  const targets = normalizeCountries(siteCountries);
  if (listed.length === 0) return { kind: "unknown" };
  if (listed.length > 1) return { kind: "mixed", countries: listed };
  const country = listed[0] as string;
  if (targets.includes(country)) return { kind: "ok", country };
  return { kind: "off-target", country, siteCountries: targets };
}

/** Métrique acceptable : `null` (inconnu → 0 au stockage) ou un nombre fini ≥ 0.
 *  NaN/Infinity/négatif = donnée corrompue → la ligne est écartée. Mêmes règles
 *  que `addRelatedKeywordsAction`. */
function isMetric(v: number | null): boolean {
  return v == null || (Number.isFinite(v) && v >= 0);
}

/** Le site du parcours, réduit à ce dont l'import a besoin. */
type ListImportSite = {
  id: string;
  primaryLanguage?: string | null;
  targetCountries?: string[] | null;
};

export type ListImportInput = {
  detail: MangoolsListDetail;
  site: ListImportSite;
  /** Slug du thème à poser sur chaque ligne (`keyword_themes.slug`), ou `null`.
   *  Le thème est créé à part (tâche 4) : ce module ne fait que le porter. */
  themeSlug: string | null;
  /** Horodatage ISO injecté — pas de `new Date()` ici, le module reste pur et
   *  ses sorties restent comparables en test. */
  now: string;
  /** `recherche` range les mots-clés dans l'étape Recherche, `reservoir` (défaut)
   *  les laisse au réservoir. Même sémantique que `addRelatedKeywordsAction`. */
  destination?: "reservoir" | "recherche";
};

export type ListImportPlan = {
  verdict: CountryVerdict;
  /** Lignes à insérer. Vide sur `mixed` / `unknown` : sans pays sûr, on ne
   *  fabrique pas de ligne. */
  rows: ListKeywordRow[];
  /** Mots-clés de la liste qui n'ont produit aucune ligne (terme vide ou trop
   *  long, slug vide, métrique absurde, doublon d'identifiant, ou verdict qui
   *  bloque l'import). Permet à l'appelant de dire la vérité sur le décompte. */
  skipped: number;
};

/** Construit la ligne d'un mot-clé, ou `null` si la donnée ne le mérite pas. */
function buildRow(
  item: MangoolsKeywordItem,
  ctx: {
    siteId: string;
    lang: string;
    country: string;
    theme: KeywordTheme | null;
    stages: KeywordStage[];
    now: string;
  },
): ListKeywordRow | null {
  if (typeof item.kw !== "string") return null;
  const term = item.kw.trim();
  if (!term || term.length > MAX_TERM_LENGTH) return null;
  if (!isMetric(item.sv) || !isMetric(item.seo) || !isMetric(item.cpc)) {
    return null;
  }
  const slug = wasm.slugify(term);
  if (!slug) return null;
  // `trend` = série de volume mensuel calculée par le normaliseur, pas un
  // tableau vide : la réponse de liste porte `msv`, contrairement au payload du
  // dialog « mots-clés associés ».
  const trend = trendFromMangools(item);
  return {
    id: keywordId(ctx.siteId, slug),
    siteId: ctx.siteId,
    term,
    lang: ctx.lang,
    country: ctx.country,
    theme: ctx.theme,
    status: "idea",
    intent: [],
    primarySource: "mangools",
    sources: ["mangools"],
    // `metricsFromMangools` porte `growthPct` (dérivé de `msv`) — recopier le
    // mapping à la main le perdrait, comme le chemin du dialog.
    metrics: { mangools: metricsFromMangools(item, ctx.now) },
    trend,
    // Sparkline PAR source, comme `seranking/sync.ts` : `trend` seul ferait une
    // courbe globale sans savoir d'où elle vient.
    trendBySource: { mangools: trend },
    stages: ctx.stages,
  };
}

/**
 * Transforme une liste KWFinder en lignes `keywords`, en tranchant d'abord la
 * question du pays.
 *
 * Rien n'est écrit ici : le retour est un plan que `list-actions.ts` insère
 * sous quota et sous verrou d'organisation.
 *
 * PORTÉE EXACTE DU VERDICT : il tranche le pays DE LA LISTE, pas celui de
 * chaque mot-clé. `detail.countries` (client.ts) est l'ensemble des codes
 * LISIBLES — un mot-clé dont la localisation est absente ou illisible n'y
 * contribue pas, et reçoit ensuite le pays du verdict comme les autres. Deux
 * conséquences assumées : `unknown` ne se déclenche que si AUCUN mot-clé de la
 * liste n'a de code, et un mot-clé sans code hérite du pays de ses voisins.
 * C'est un héritage, pas un repli sur le site — la règle « le pays n'est jamais
 * supposé » porte sur le fait de ne pas inventer un pays que la liste ne dit
 * nulle part, et une liste `mixed` ne produit toujours AUCUNE ligne.
 */
export function planListImport(input: ListImportInput): ListImportPlan {
  const { detail, site, themeSlug, now } = input;
  const verdict = resolveListCountry(
    detail.countries,
    site.targetCountries ?? [],
  );
  // `mixed` / `unknown` : aucune ligne. Le verdict remonte pour être affiché ;
  // choisir un pays « par défaut » reviendrait à décider à la place de
  // l'utilisateur, ce que cette tâche existe précisément pour ne pas faire.
  if (verdict.kind === "mixed" || verdict.kind === "unknown") {
    return { verdict, rows: [], skipped: detail.keywords.length };
  }

  const ctx = {
    siteId: site.id,
    lang: site.primaryLanguage || "en",
    country: verdict.country,
    // Le slug d'un thème est libre et par site (`keyword_themes`) ; le type
    // `KeywordTheme` est une union héritée que la colonne porte encore — même
    // cast que `theme-context.tsx`.
    theme: (themeSlug || null) as KeywordTheme | null,
    stages: (input.destination === "recherche"
      ? ["research"]
      : []) as KeywordStage[],
    now,
  };

  // Déduplication par identifiant calculé : deux termes peuvent slugifier vers
  // le même id (« Bague Or » / « bague  or ») — le premier gagne, comme dans
  // `addRelatedKeywordsAction`.
  const byId = new Map<string, ListKeywordRow>();
  for (const item of detail.keywords) {
    const row = buildRow(item, ctx);
    if (!row || byId.has(row.id)) continue;
    byId.set(row.id, row);
  }
  const rows = [...byId.values()];
  return { verdict, rows, skipped: detail.keywords.length - rows.length };
}
