"use client";

import { useEffect, useRef, useState, type ReactNode } from "react";
import { Spinner } from "@/components/app/spinner";
import { jobStatusAction } from "@/components/jobs/actions";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { isJobFinished, JOB_STATUS_LABELS, type Job } from "@/lib/jobs";
import { nextPollStep, type PollOutcome } from "@/lib/poll";

// Suivi d'un job du socle : polling, statut, progression, rapport final (spec Imports §7.3).
// Construit **une fois** et réutilisable tel quel par Crawl et GSC : il ne sait rien du
// module qui a enfilé le job. Ce que `progress` et `report` contiennent est du jsonb libre,
// rendu par les deux fonctions que l'appelant fournit — aucune clé métier n'est câblée ici.

const STATUS_VARIANTS: Record<string, "secondary" | "outline" | "destructive"> = {
  queued: "outline",
  running: "outline",
  done: "secondary",
  failed: "destructive",
};

type JobWatcherProps = {
  jobId: string;
  /** Intervalle de polling ; ~2 s suffit pour un import (spec §7.3). */
  intervalMs?: number;
  /** Rendu de `job.progress` tant que le job tourne. */
  renderProgress?: (job: Job) => ReactNode;
  /** Rendu de `job.report` quand le job est terminé. */
  renderReport?: (job: Job) => ReactNode;
  /** Appelé une fois, au passage à `done` ou `failed` (rafraîchir une liste, par exemple). */
  onFinished?: (job: Job) => void;
};

export function JobWatcher({
  jobId,
  intervalMs = 2000,
  renderProgress,
  renderReport,
  onFinished,
}: JobWatcherProps) {
  const [job, setJob] = useState<Job | null>(null);
  const [error, setError] = useState<string | null>(null);
  // Une erreur **est** l'état « suivi arrêté » : elle gouverne l'effet, et l'effacer (bouton
  // « Réessayer ») suffit à rejouer un cycle de polling neuf. Pas de compteur de tentatives
  // en dépendance fantôme.
  const stopped = error !== null;
  // Le callback vit dans une ref : sans cela, un parent qui le recrée à chaque rendu
  // relancerait le polling en boucle. La ref s'écrit après le rendu, jamais pendant.
  const finished = useRef(onFinished);
  useEffect(() => {
    finished.current = onFinished;
  }, [onFinished]);

  useEffect(() => {
    if (stopped) return;
    let cancelled = false;
    let timer: ReturnType<typeof setTimeout> | undefined;
    // Échecs **de transport** consécutifs. Remis à zéro dès qu'une réponse arrive : un
    // hoquet réseau isolé sur un job de deux minutes ne doit pas condamner le suivi.
    let failures = 0;

    const read = async (): Promise<PollOutcome<Job>> => {
      try {
        const result = await jobStatusAction(jobId);
        // Un refus de l'api (404 : ce job n'existe pas) ne s'améliorera jamais.
        return "error" in result
          ? { status: "refused", error: result.error }
          : { status: "value", value: result.job };
      } catch {
        // L'appel de la Server Action lui-même a échoué : coupure réseau, dev server
        // redémarré, proxy. Sans ce catch, la promesse rejetait, plus aucun `setTimeout`
        // n'était posé, et l'écran restait sur « En cours » **sans message** — le job,
        // lui, avançait très bien côté serveur.
        return { status: "unreachable", error: "l'application n'a pas pu joindre le serveur" };
      }
    };

    const tick = async () => {
      const outcome = await read();
      if (cancelled) return;
      if (outcome.status === "unreachable") failures += 1;
      else failures = 0;
      // `setError(null)` n'est pas appelé ici : `error` est déjà `null` tant que l'effet
      // tourne (`stopped` le garde), et le réécrire relancerait l'effet à chaque tick.
      if (outcome.status === "value") setJob(outcome.value);
      const step = nextPollStep(outcome, (value: Job) => isJobFinished(value.status), failures);
      if (step.action === "stop") {
        if (outcome.status === "value") finished.current?.(outcome.value);
        return;
      }
      if (step.action === "fail") {
        setError(step.error);
        return;
      }
      timer = setTimeout(() => void tick(), intervalMs);
    };

    void tick();
    return () => {
      cancelled = true;
      if (timer !== undefined) clearTimeout(timer);
    };
  }, [jobId, intervalMs, stopped]);

  const status = job?.status ?? "queued";
  const running = !isJobFinished(status);
  const watching = running && !stopped;

  return (
    <div className="space-y-3 rounded-lg border p-4">
      <div className="flex flex-wrap items-center gap-2">
        <Badge variant={STATUS_VARIANTS[status] ?? "outline"}>
          {JOB_STATUS_LABELS[status] ?? status}
        </Badge>
        {watching ? (
          <span className="text-muted-foreground text-sm">
            Traitement en cours, cette page se met à jour toute seule.
          </span>
        ) : null}
      </div>

      {/* Aucune source ne donne le nombre total de lignes avant la fin du fichier : une
          barre de progression mentirait, et l'écran « Import lancé » restait trop figé. Le
          spinner dit que ça tourne, les compteurs juste en dessous disent où ça en est. */}
      {watching ? (
        <div className="flex justify-center py-4">
          <Spinner label="Traitement en cours" />
        </div>
      ) : null}

      {job && running ? renderProgress?.(job) : null}
      {job && !running ? renderReport?.(job) : null}

      {job?.error ? (
        <p role="alert" className="text-destructive text-sm">
          {job.error}
        </p>
      ) : null}
      {error ? (
        // Le suivi est mort, pas le job : le dire, et laisser reprendre le suivi.
        <div role="alert" className="space-y-2">
          <p className="text-destructive text-sm">
            Suivi interrompu : {error}. Le traitement continue côté serveur.
          </p>
          <Button type="button" variant="outline" size="sm" onClick={() => setError(null)}>
            Réessayer
          </Button>
        </div>
      ) : null}
    </div>
  );
}
