import { formatLabel, reportSummary, toImportReport, type ImportEntry } from "@/lib/imports";
import { isJobFinished } from "@/lib/jobs";
import { keywordsHref } from "@/lib/keywords-scope";

// Décision des toasts de travail en arrière-plan, isolée du composant pour être testable
// sans navigateur — même partage que `lib/poll.ts` pour le suivi de job d'une page.
//
// Règle du 2026-09-01 : **tout travail de fond doit être visible**, où que l'utilisateur se
// trouve dans l'application. Le provider `JobsToaster` interroge les sources toutes les ~10 s
// et ne fait qu'appliquer les actions calculées ici : c'est cette fonction qui décide de ce
// qu'on ouvre, met à jour, conclut ou ferme.

/**
 * Un travail de fond suivi par la couche de notification, quelle qu'en soit la source.
 *
 * Volontairement plat et sans champ métier : les imports le remplissent aujourd'hui, Crawl et
 * Search Console le rempliront demain sans que le provider change d'une ligne.
 */
export type TrackedJob = {
  /** Identité stable du toast d'un tour de polling à l'autre. */
  id: string;
  /** Statut du socle jobs (`queued`, `running`, `done`, `failed`). */
  status: string;
  /** Titre du toast : le nom du fichier, ou ce qui en tient lieu. */
  title: string;
  /** Ligne secondaire : progression tant que ça tourne, résumé ou erreur à la fin. */
  detail: string | null;
  /** Où mène le toast une fois le travail terminé (le rapport), s'il y a une page. */
  href: string | null;
  error: string | null;
};

export type ToastAction =
  /** Ouvrir ou mettre à jour le toast persistant d'un travail en cours. */
  | { kind: "loading"; job: TrackedJob }
  /** Conclure : le travail suivi vient de se terminer. */
  | { kind: "success"; job: TrackedJob }
  | { kind: "error"; job: TrackedJob }
  /** Fermer un toast dont le travail n'est plus observable. */
  | { kind: "dismiss"; id: string };

/**
 * Que faire des toasts, connaissant les travaux **déjà suivis** et l'état courant des sources.
 *
 * Trois décisions, et c'est la troisième qui compte :
 *
 * - un travail actif ouvre (ou rafraîchit) son toast persistant ;
 * - un travail terminé **qu'on suivait** conclut son toast — succès ou échec ;
 * - un travail terminé qu'on ne suivait pas est **ignoré** : sans cette règle, ouvrir
 *   l'application ferait pleuvoir un toast par import de la première page du listing.
 *
 * Un travail suivi qui disparaît des sources (sorti de la première page, poussé par des
 * imports plus récents) voit son toast fermé : à durée infinie, il resterait sinon affiché
 * pour toujours sans que rien ne vienne jamais le conclure.
 */
export function reconcileToasts(
  tracked: ReadonlySet<string>,
  jobs: readonly TrackedJob[],
): { actions: ToastAction[]; tracked: Set<string> } {
  const actions: ToastAction[] = [];
  const next = new Set<string>();
  const seen = new Set<string>();
  for (const job of jobs) {
    seen.add(job.id);
    if (!isJobFinished(job.status)) {
      actions.push({ kind: "loading", job });
      next.add(job.id);
      continue;
    }
    if (tracked.has(job.id)) {
      actions.push({ kind: job.status === "failed" ? "error" : "success", job });
    }
  }
  for (const id of tracked) {
    if (!seen.has(id)) actions.push({ kind: "dismiss", id });
  }
  return { actions, tracked: next };
}

/**
 * Vue « travail de fond » d'une entrée du listing des imports.
 *
 * `progress` est le `progress` du job du socle (`GET /jobs/{id}`) : le listing des imports ne
 * porte que le rapport **final**, la progression ne peut donc venir que de là. `siteId` est
 * résolu par l'appelant depuis la cible, parce que l'api des imports ne rend que
 * `site_target_id` et que la page de rapport vit sous `/sites/<id>/…`.
 */
export function importToTrackedJob(
  entry: ImportEntry,
  siteId: string | null,
  progress: Record<string, unknown> | null,
): TrackedJob {
  const reconcile = entry.entry === "map_reconcile";
  const title = reconcile
    ? "Rattachement automatique"
    : (entry.filename ?? formatLabel(entry.source));
  return {
    id: entry.id,
    status: entry.status,
    title,
    detail: jobDetail(entry, progress),
    // Un rattachement n'a pas de page de rapport (§12.2) : pas de lien plutôt qu'un 404.
    href: reconcile || siteId === null ? null : keywordsHref(siteId, `imports/${entry.id}`),
    error: entry.error,
  };
}

/**
 * Ce qu'on annonce tant que le worker n'a **rien publié**, dans les mots du travail en cours.
 *
 * Par nature de travail, et non un libellé unique : un rattachement automatique n'a pas de
 * fichier à préparer, et le crawl comme Search Console n'en auront pas davantage. Un type
 * absent de cette table retombe sur le générique — c'est ce qui les fera arriver propres.
 */
const STARTING_DETAIL: Record<string, string> = {
  import: "Préparation du fichier…",
  //  Un import API n'a pas de fichier à préparer : il attend le fournisseur. Distingué par le
  //  `kind` de l'import et non par son `entry`, qui vaut « import » pour les deux — trois
  //  toasts « Préparation du fichier… » pour trois listes KWFinder décriraient un travail qui
  //  n'a pas lieu.
  "import:api": "Interrogation du fournisseur…",
  map_reconcile: "Rattachement en cours…",
};

const GENERIC_STARTING_DETAIL = "Traitement en cours…";

function jobDetail(entry: ImportEntry, progress: Record<string, unknown> | null): string | null {
  if (entry.status === "failed") return entry.error;
  if (isJobFinished(entry.status)) {
    return entry.entry === "import" ? reportSummary(toImportReport(entry.report)) : null;
  }
  // `rows_read` est un compteur du pipeline CSV : il ne veut rien dire ailleurs.
  const read = entry.entry === "import" ? progress?.["rows_read"] : undefined;
  if (typeof read === "number") return `${read} lignes lues`;
  const kind = entry.entry === "import" ? `${entry.entry}:${entry.kind}` : entry.entry;
  return STARTING_DETAIL[kind] ?? STARTING_DETAIL[entry.entry] ?? GENERIC_STARTING_DETAIL;
}
