import type { CountryCount } from "@/lib/imports";
import { formatTarget } from "@/lib/locales";
import type { Target } from "@/lib/sites";

// Décision de l'étape 3 du wizard d'import quand le fichier porte un pays par ligne (§4.2,
// §4.4) : quels pays sont proposés, quelles cibles sont réutilisées, lesquelles sont à créer,
// et **combien de jobs** partent sur le même fichier déjà envoyé.
//
// Isolé du composant pour la même raison que `lib/job-toasts.ts` : c'est une machine d'état,
// pas de la mise en page, et elle doit s'éprouver sans navigateur.
//
// Deux règles gouvernent tout le module :
//
// 1. **Le libellé d'un pays vient de l'api**, jamais de `COUNTRIES` (`lib/locales.ts`) : cette
//    table de trente entrées ignore AU, SG, HK, NZ, IL et JP — six des onze marchés d'un seul
//    export KWFinder réel — et son `knownCountry` remplace *silencieusement* tout code absent
//    par `FR`. `CountryCount.label` est non nul par contrat et nomme les 249 pays ISO.
// 2. **Réutiliser plutôt que recréer.** `POST /sites/{id}/targets` rend un 409 sur un couple
//    (pays, langue) existant et ce 409 **ne porte pas** l'identifiant de la cible existante :
//    l'encaisser n'apprendrait rien. La cible est donc cherchée dans la liste du site *avant*
//    toute création.

/**
 * Plafond de cibles par site — miroir de `MAX_TARGETS_PER_SITE` (`api/app/sites/schemas.py`).
 *
 * Compté **avant** la boucle de création : onze pays cochés sur un site qui en porte déjà dix
 * déclencheraient sinon un 422 en plein milieu, cibles à moitié créées et rien qui le dise.
 */
export const MAX_TARGETS_PER_SITE = 20;

/**
 * Contraintes de casse de l'api : `ck_site_targets_country` / `ck_site_targets_language`.
 *
 * Exportées pour `lib/import-files.ts`, qui rejoue la même vérification sur le recensement
 * fusionné de N fichiers — une langue manquante y est un blocage commun, pas un blocage par
 * fichier, et deux expressions régulières jumelles finiraient par diverger.
 */
export const COUNTRY_CODE = /^[A-Z]{2}$/;
export const LANGUAGE_CODE = /^[a-z]{2}$/;

/** Un pays du recensement, dédoublonné par code. */
export type CensusCountry = {
  /** ISO 3166-1 alpha-2, **majuscules** — la casse qu'exige l'api. */
  code: string;
  /** Nom français servi par l'api (`CountryCount.label`). */
  label: string;
  /** Libellés bruts fusionnés sous ce code (« United States », « USA »). */
  rawLabels: string[];
  /** Total des lignes du fichier pour ce code. */
  rows: number;
  /** Langue proposée par CLDR, ou `null` — 8 territoires ISO sur 249 n'en ont pas (dont PH). */
  suggestedLanguage: string | null;
};

/** Un libellé que l'api n'a pas su résoudre : une localité (« New York »), un nom inconnu. */
export type CensusUnresolved = { raw: string; label: string; rows: number };

export type Census = { countries: CensusCountry[]; unresolved: CensusUnresolved[] };

/**
 * Recensement de l'aperçu, prêt à afficher et à cocher.
 *
 * `PreviewOut.countries` porte **une entrée par libellé brut distinct**, pas par code : deux
 * orthographes du même pays (« United States », « USA ») y font deux lignes qui rendent le même
 * `US`. Sans ce dédoublonnage, cocher les deux créerait la cible une fois puis prendrait un 409
 * sur la seconde. Les lignes sont fusionnées (décomptes additionnés) et l'ordre de l'api — lignes
 * décroissantes, contractuel — est conservé par la première occurrence de chaque code.
 *
 * Les libellés sans code sortent à part : ils sont montrés pour révéler le trou, jamais proposés.
 */
export function census(entries: readonly CountryCount[]): Census {
  const countries: CensusCountry[] = [];
  const byCode = new Map<string, CensusCountry>();
  const unresolved: CensusUnresolved[] = [];
  for (const entry of entries) {
    if (entry.code === null) {
      unresolved.push({ raw: entry.raw, label: entry.label, rows: entry.rows });
      continue;
    }
    const code = entry.code.toUpperCase();
    const language = entry.suggested_language?.toLowerCase() ?? null;
    const known = byCode.get(code);
    if (known === undefined) {
      const country: CensusCountry = {
        code,
        label: entry.label,
        rawLabels: [entry.raw],
        rows: entry.rows,
        suggestedLanguage: language,
      };
      byCode.set(code, country);
      countries.push(country);
      continue;
    }
    known.rawLabels.push(entry.raw);
    known.rows += entry.rows;
    known.suggestedLanguage ??= language;
  }
  return { countries, unresolved };
}

/** Ce que l'utilisateur a dit d'un pays du recensement : coché ? et dans quelle langue ? */
export type CountryChoice = { selected: boolean; language: string };

/**
 * État d'une ligne du recensement à laquelle l'utilisateur n'a pas touché.
 *
 * Le pays de la cible courante est **pré-coché dans sa langue** : c'est l'import que le wizard
 * lançait déjà avant le cloisonnement, il ne doit pas se perdre. Les autres pays sont proposés
 * dans la langue d'une cible existante du site quand il y en a une (rester sur `en-GB` plutôt
 * que d'en créer une seconde), sinon dans celle que CLDR propose, sinon vides — auquel cas la
 * langue est à choisir et le lancement reste bloqué tant qu'elle ne l'est pas.
 */
export function defaultChoice(
  country: CensusCountry,
  targets: readonly Target[],
  principal: Target | undefined,
): CountryChoice {
  if (principal !== undefined && principal.country === country.code) {
    return { selected: true, language: principal.language };
  }
  const existing = targets.find((target) => target.country === country.code);
  return { selected: false, language: existing?.language ?? country.suggestedLanguage ?? "" };
}

/**
 * Le message d'un fichier dont la colonne pays ne résout **rien** (§4.3).
 *
 * Il nomme deux libellés lus dans le fichier : sans eux, « aucun pays reconnu » laisse
 * l'utilisateur devant une colonne qu'il croit bonne. Avec « New York » sous les yeux, il
 * comprend en une seconde que sa colonne porte des villes.
 */
export function unreadableCountriesMessage(unresolved: readonly CensusUnresolved[]): string {
  const examples = unresolved
    .slice(0, 2)
    .map((item) => `« ${item.label} »`)
    .join(", ");
  return `Aucun pays reconnu dans la colonne pays : ${unresolved.length} ${
    unresolved.length > 1 ? "valeurs non reconnues" : "valeur non reconnue"
  } (${examples}). Corrigez la colonne, ou retirez ce fichier du lot.`;
}

/** Un import à lancer : une cible, une liste, un job. */
export type PlannedImport = {
  /** Clé stable de rendu et de suivi : le code du pays, ou `principal` sans recensement. */
  key: string;
  /** Cible à réutiliser ; `null` quand elle reste à créer. */
  targetId: string | null;
  /** ISO 3166-1 alpha-2 majuscule. */
  country: string;
  /** ISO 639-1 minuscule. */
  language: string;
  /** Libellé de la cible, tel qu'il s'affiche. */
  label: string;
  /** Destination : liste existante par `id`, ou liste à créer par son `name`. */
  list: { id: string } | { name: string };
};

export type LaunchPlan = {
  imports: PlannedImport[];
  /** Nombre de cibles à créer avant de lancer — ce qui compte pour le plafond du site. */
  creations: number;
  /** Motifs qui interdisent le lancement. Vide = le bouton est actif. */
  blockers: string[];
  /**
   * Libellé de la cible courante quand le fichier ne porte **aucune** ligne pour son pays
   * (§4.3) : son import serait vide, il n'est pas planifié. `null` le reste du temps.
   */
  droppedTarget: string | null;
};

/**
 * Ce que « Lancer l'import » va faire, et ce qui l'en empêche.
 *
 * Sans recensement (Semrush, CSV générique sans colonne pays), le plan est celui d'avant : la
 * cible du sélecteur, un job. Avec recensement, **le fichier commande** — un job par pays coché,
 * sur le même `sha256` déjà envoyé, donc sans aucun re-transfert.
 *
 * Le lancement n'est **pas** atomique : `POST /imports` publie chaque import par son propre
 * commit. Ce plan sert donc aussi de liste d'états à l'écran, un par cible.
 */
export function launchPlan({
  targets,
  targetId,
  countries,
  unresolved = [],
  choice,
  listName,
  existingListId,
}: {
  /** Cibles du site, **fusionnées** avec celles créées depuis l'écran. */
  targets: readonly Target[];
  /** Cible du sélecteur : celle qui est pré-cochée et dont les listes ont été chargées. */
  targetId: string;
  countries: readonly CensusCountry[];
  /**
   * Libellés que l'api n'a pas su résoudre. Ils ne sont jamais importables, mais leur seule
   * présence change le sens d'un recensement vide : voir plus bas.
   */
  unresolved?: readonly CensusUnresolved[];
  choice: (country: CensusCountry) => CountryChoice;
  /** Nom de base des listes à créer, tel que saisi. */
  listName: string;
  /** Liste existante choisie pour la cible du sélecteur, ou `null` (« nouvelle liste »). */
  existingListId: string | null;
}): LaunchPlan {
  const current = targets.find((target) => target.id === targetId);
  const imports: PlannedImport[] = [];
  const blockers: string[] = [];

  if (countries.length === 0 && unresolved.length > 0) {
    // **Pas** la même chose qu'un fichier sans colonne pays (§4.3). La colonne est là, elle
    // est simplement illisible : que des localités (« New York »), ou des libellés hors
    // tables. Retomber sur l'import « principal » lancerait un job dont on sait déjà qu'il
    // ignorera TOUTES les lignes — zéro insérée, un rapport vide, et personne pour dire
    // pourquoi. Le lancement est donc refusé pour ce fichier, en nommant ce qu'on a lu.
    blockers.push(unreadableCountriesMessage(unresolved));
  } else if (countries.length === 0) {
    // Le fichier ne dit rien du pays de ses lignes : rien à cocher, rien à cloisonner.
    if (current !== undefined) {
      imports.push({
        key: "principal",
        targetId: current.id,
        country: current.country,
        language: current.language,
        label: formatTarget(current),
        list: { name: listName },
      });
    }
  } else {
    // Un couple (pays, langue) ne peut donner qu'une cible : `UNIQUE (site_id, country,
    // language)`. Deux lignes qui y mèneraient se fondent en un seul import.
    const planned = new Set<string>();
    for (const country of countries) {
      const picked = choice(country);
      if (!picked.selected) continue;
      const language = picked.language.trim().toLowerCase();
      if (!LANGUAGE_CODE.test(language) || !COUNTRY_CODE.test(country.code)) {
        blockers.push(`Choisissez une langue pour ${country.label}.`);
        continue;
      }
      const couple = `${country.code}/${language}`;
      if (planned.has(couple)) continue;
      planned.add(couple);
      const existing = targets.find(
        (target) => target.country === country.code && target.language === language,
      );
      imports.push({
        key: country.code,
        targetId: existing?.id ?? null,
        country: country.code,
        language,
        label: formatTarget({ country: country.code, language }),
        list: { name: listName },
      });
    }
  }

  // Réutiliser une liste existante suppose de l'avoir chargée, et le wizard ne charge que
  // celles de la cible du sélecteur. Deux conditions, donc : un seul import, et il vise
  // cette cible-là. Dès qu'un second pays est retenu, tout se donne par le nom — c'est aussi
  // ce que l'écran promet à ce moment-là, il ne doit pas se démentir en silence.
  const only = imports.length === 1 ? imports[0] : undefined;
  const resolved =
    only !== undefined && existingListId !== null && only.targetId === targetId
      ? [{ ...only, list: { id: existingListId } }]
      : imports;

  // Le nom est vérifié **avant** le suffixe : « — GB » suffirait sinon à le rendre non vide.
  if (resolved.some((entry) => "name" in entry.list && entry.list.name.trim().length === 0)) {
    blockers.push("Nommez la liste de destination.");
  }

  // Une liste appartient à une cible (`UNIQUE (site_target_id, name)`) : N listes homonymes ne
  // violent rien côté base, mais elles sont indiscernables à l'écran et dans le réservoir. Dès
  // qu'il y a plus d'un import, chaque nom porte donc son pays. Composé **ici** et non dans
  // `suggestedListName` (`lib/imports.ts`), figée par l'unité B0.
  const named =
    resolved.length > 1
      ? resolved.map((entry) =>
          "name" in entry.list
            ? { ...entry, list: { name: `${entry.list.name} — ${entry.country}` } }
            : entry,
        )
      : resolved;

  // Une colonne pays illisible a déjà tout dit : « cochez un pays » n'aurait aucun sens
  // au-dessus d'une liste vide de cases à cocher.
  if (named.length === 0 && !(countries.length === 0 && unresolved.length > 0)) {
    blockers.unshift(
      current !== undefined && countries.every((country) => country.code !== current.country)
        ? `Le fichier ne contient aucune ligne pour ${formatTarget(current)} : cet import serait vide. Cochez un pays présent dans le fichier.`
        : "Cochez au moins un pays à importer.",
    );
  }

  const creations = named.filter((entry) => entry.targetId === null).length;
  if (targets.length + creations > MAX_TARGETS_PER_SITE) {
    blockers.push(
      `Ce site atteindrait le plafond de ${MAX_TARGETS_PER_SITE} cibles (${targets.length} existantes, ${creations} à créer) : décochez des pays.`,
    );
  }

  return {
    imports: named,
    creations,
    blockers,
    droppedTarget:
      current !== undefined &&
      countries.length > 0 &&
      countries.every((country) => country.code !== current.country)
        ? formatTarget(current)
        : null,
  };
}
