import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Spinner } from '@/components/ui/spinner';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { useTranslation } from '@/hooks/use-translation';
import { cn } from '@/lib/utils';
import { router } from '@inertiajs/react';
import { AlertTriangle, Ban, CheckCircle2, ChevronRight, Clock, RefreshCcw, Send, SkipForward, XCircle } from 'lucide-react';
import * as React from 'react';
import { toast } from 'sonner';
import { fmtDateFr, fmtMillimes, libelleMois } from '../format';
import { appelJson, messageDuCorps, raisonEchec, remplir, urlRoute } from './api';
import { LegendeCouleurs } from './legende-couleurs';
import {
    bilanLot,
    creerLotRunner,
    lignesRestantes,
    lotExpire,
    progressionLot,
    type EtatLot,
    type LotRunner,
    type ResultatLigneLot,
} from './lot-runner';
import type { AEnvoyer, LigneAEnvoyer, LotCree, ResultatDepot } from './types';

interface Props {
    open: boolean;
    onOpenChange: (open: boolean) => void;
}

/**
 * ENVOYER LES LIGNES IMPORTÉES À LA DGI — une déclaration réelle par ligne.
 *
 * ═════════════════════════════════════════════════════════════════════════════
 * UN GESTE, N DÉPÔTS, UN À LA FOIS
 * ═════════════════════════════════════════════════════════════════════════════
 * « Envoyer N lignes » crée d'abord un LOT côté serveur (la liste exacte des
 * lignes cochées, et la case « J'ai vérifié » qui l'accompagne) ; l'écran
 * enchaîne ensuite UN dépôt par ligne, en attendant chacun — jamais deux en vol
 * (`lot-runner.ts`, testé). La première erreur arrête tout et se montre :
 * « Reprendre » retente la ligne en échec, puis continue. Un lot expiré est
 * recréé avec les lignes qui restent.
 *
 * Fermer pendant l'envoi demande confirmation : la ligne en vol se termine,
 * les suivantes ne partent pas.
 */
export function EnvoiLotDialog({ open, onOpenChange }: Props) {
    const { t } = useTranslation();
    /** `t` change d'identité à chaque rendu : le chargement d'ouverture la lit ici (voir `ImportSheetDialog`). */
    const traduire = React.useRef(t);
    React.useEffect(() => {
        traduire.current = t;
    });

    const [donnees, setDonnees] = React.useState<AEnvoyer | null>(null);
    const [chargement, setChargement] = React.useState(false);
    const [erreurChargement, setErreurChargement] = React.useState<string | null>(null);
    const [decochees, setDecochees] = React.useState<Set<number>>(new Set());
    const [verifie, setVerifie] = React.useState(false);

    const [creationLot, setCreationLot] = React.useState(false);
    const [erreurLot, setErreurLot] = React.useState<string | null>(null);
    const [etatLot, setEtatLot] = React.useState<EtatLot | null>(null);

    const runner = React.useRef<LotRunner | null>(null);
    const lot = React.useRef<{ id: string; expireA: string } | null>(null);

    const enCours = etatLot?.statut === 'en_cours';

    /*
     * Un envoi qui s'arrête (erreur, ligne écartée, arrêt demandé) ou qui se
     * termine ramène le corps du dialogue en HAUT : la barre de progression et
     * le bandeau d'arrêt ou le bilan y sont, et la liste, défilée jusqu'en bas
     * pendant l'envoi, les cachait. Rien ne défile pendant l'envoi lui-même.
     */
    const corps = React.useRef<HTMLDivElement>(null);
    const statutLot = etatLot?.statut ?? null;
    React.useEffect(() => {
        if (statutLot !== 'arrete' && statutLot !== 'termine') return;

        corps.current?.scrollTo({ top: 0, behavior: 'smooth' });
    }, [statutLot]);

    const charger = React.useCallback(async () => {
        setChargement(true);
        setErreurChargement(null);

        const reponse = await appelJson<AEnvoyer>(urlRoute('retenue.import_sheet.a_envoyer'));
        setChargement(false);

        if (!reponse.ok) {
            setErreurChargement(raisonEchec(reponse, traduire.current));
            return;
        }

        setDonnees(reponse.donnees);
    }, []);

    // Ouverture : page blanche et liste relue — SAUF si un envoi tourne encore
    // (fermé en cours de route) : on retrouve alors sa progression.
    React.useEffect(() => {
        if (!open || runner.current?.enCours()) return;

        runner.current = null;
        lot.current = null;
        setDonnees(null);
        setDecochees(new Set());
        setVerifie(false);
        setCreationLot(false);
        setErreurLot(null);
        setEtatLot(null);

        void charger();
    }, [open, charger]);

    // Quitter la page pendant un envoi : le navigateur demande confirmation.
    React.useEffect(() => {
        if (!enCours) return;

        const retenir = (e: BeforeUnloadEvent) => {
            e.preventDefault();
        };
        window.addEventListener('beforeunload', retenir);

        return () => window.removeEventListener('beforeunload', retenir);
    }, [enCours]);

    const lignes = React.useMemo(() => donnees?.lignes ?? [], [donnees]);
    const parId = React.useMemo(() => new Map(lignes.map((l) => [l.id, l])), [lignes]);
    const choisies = lignes.filter((l) => !decochees.has(l.id)).map((l) => l.id);

    /** Crée un lot côté serveur — `null` (et l'erreur affichée) s'il est refusé. */
    const creerLot = async (ids: number[]): Promise<LotCree | null> => {
        setCreationLot(true);
        setErreurLot(null);

        const reponse = await appelJson<LotCree>(urlRoute('retenue.import_sheet.lots.store'), {
            methode: 'POST',
            corps: { lignes: ids, verifie: true },
        });
        setCreationLot(false);

        if (!reponse.ok) {
            setErreurLot(raisonEchec(reponse, t));
            return null;
        }

        lot.current = { id: reponse.donnees.lot, expireA: reponse.donnees.expire_a };

        return reponse.donnees;
    };

    /** UN dépôt — branché sur l'orchestrateur, qui attend chacun. */
    const deposer = async (ligneId: number): Promise<ResultatDepot> => {
        const courant = lot.current;
        if (!courant) return { ok: false, message: t('retenue', 'lot_err_sans_lot', 'Aucun lot en cours : relancez l’envoi.'), code: null };

        const reponse = await appelJson<ResultatDepot>(urlRoute('retenue.import_sheet.lots.deposer', { lot: courant.id, ligne: ligneId }), {
            methode: 'POST',
            corps: {},
        });

        if (reponse.ok) {
            return reponse.donnees.ok ? reponse.donnees : { ok: false, message: reponse.donnees.message, code: reponse.donnees.code ?? null };
        }

        if (reponse.statut === 429) {
            return {
                ok: false,
                message: t('retenue', 'lot_err_limite', 'Trop d’envois en peu de temps. Patientez une minute, puis « Reprendre ».'),
                code: 'limite',
            };
        }

        // 422 : `{ ok: false, message, code }` — le refus dit par le serveur.
        const corps = reponse.corps as { code?: unknown } | null;
        const message = reponse.statut === 422 ? messageDuCorps(reponse.corps) : null;

        return {
            ok: false,
            message: message ?? raisonEchec(reponse, t),
            code: typeof corps?.code === 'string' ? corps.code : null,
        };
    };

    /** Les lignes closes (envoyées + écartées) — ce qui, en changeant, justifie de relire la page. */
    const closes = (etat: EtatLot) => {
        const bilan = bilanLot(etat);

        return bilan.envoyees + bilan.ecartees;
    };

    /** Fin d'un passage (terminé ou arrêté) : la page relit ses lignes si une ligne a été envoyée ou écartée. */
    const apresPassage = (avant: number, fin: EtatLot) => {
        if (fin.statut === 'termine') {
            const bilan = bilanLot(fin);
            toast.success(
                remplir(t('retenue', 'lot_toast_fini', ':n ligne(s) envoyée(s) à la DGI, :m écartée(s).'), { n: bilan.envoyees, m: bilan.ecartees }),
            );
        }
        if (closes(fin) > avant) router.reload();
    };

    const envoyer = async () => {
        if (choisies.length === 0 || !verifie) return;

        const cree = await creerLot(choisies);
        if (!cree) return;

        const orchestrateur = creerLotRunner(cree.lignes, deposer, setEtatLot);
        runner.current = orchestrateur;
        setEtatLot(orchestrateur.etat());

        apresPassage(0, await orchestrateur.lancer());
    };

    const reprendre = async () => {
        const orchestrateur = runner.current;
        if (!orchestrateur || orchestrateur.enCours()) return;

        const etat = orchestrateur.etat();
        const restantes = lignesRestantes(etat);

        // Lot expiré (le serveur l'a dit, ou l'heure est passée) : on en recrée
        // un avec ce qui reste — la vérification porte sur les mêmes lignes.
        // Plus rien à envoyer : rien à recréer, la reprise termine le lot.
        if (restantes.length > 0 && lotExpire(lot.current?.expireA ?? null, etat.erreur, Date.now())) {
            const cree = await creerLot(restantes);
            if (!cree) return;
        }

        apresPassage(closes(etat), await orchestrateur.reprendre());
    };

    /**
     * « Passer cette ligne » : la ligne en erreur est écartée — elle ne
     * repartira pas d'ici — et l'envoi reprend avec les suivantes.
     */
    const passer = async () => {
        const orchestrateur = runner.current;
        if (!orchestrateur || orchestrateur.enCours()) return;

        orchestrateur.ecarterLigneEnErreur();
        await reprendre();
    };

    const changerOuverture = (valeur: boolean) => {
        if (valeur) {
            onOpenChange(true);
            return;
        }

        // Le lot est en cours de création : fermer maintenant laisserait
        // l'envoi démarrer derrière un dialogue fermé.
        if (creationLot) return;

        if (runner.current?.enCours()) {
            const ok = window.confirm(
                t(
                    'retenue',
                    'lot_confirmer_fermeture',
                    'Un envoi est en cours. Fermer arrêtera l’envoi après la ligne en cours ; les lignes suivantes ne partiront pas. Fermer quand même ?',
                ),
            );
            if (!ok) return;
            runner.current.arreter();
        }

        onOpenChange(false);
    };

    const basculer = (id: number) => {
        setDecochees((avant) => {
            const suivant = new Set(avant);
            if (suivant.has(id)) suivant.delete(id);
            else suivant.add(id);

            return suivant;
        });
    };

    const progression = etatLot ? progressionLot(etatLot) : null;
    const bilan = etatLot ? bilanLot(etatLot) : null;
    const lignesAffichees: LigneAEnvoyer[] = etatLot
        ? etatLot.ordre.map((id) => parId.get(id)).filter((l): l is LigneAEnvoyer => l !== undefined)
        : lignes;
    const bloquees = donnees?.bloquees ?? [];

    return (
        <Dialog open={open} onOpenChange={changerOuverture}>
            <DialogContent
                className="flex max-h-[90vh] flex-col gap-3 p-4 sm:max-w-4xl sm:p-6"
                onInteractOutside={(e) => e.preventDefault()}
                onEscapeKeyDown={(e) => {
                    if (enCours) e.preventDefault();
                }}
            >
                <DialogHeader>
                    <DialogTitle className="flex items-center gap-2">
                        <Send className="h-5 w-5" />
                        {t('retenue', 'lot_titre', 'Envoyer les lignes importées à la DGI')}
                    </DialogTitle>
                    <DialogDescription>
                        {t(
                            'retenue',
                            'lot_description',
                            'Chaque ligne part seule, l’une après l’autre : chaque envoi est une déclaration définitive. La première erreur arrête l’envoi ; corrigez puis reprenez.',
                        )}
                    </DialogDescription>
                </DialogHeader>

                <div ref={corps} className="-mx-1 flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto px-1 pb-1">
                    {chargement && (
                        <p className="text-muted-foreground flex items-center gap-2 py-6 text-sm">
                            <Spinner size="sm" />
                            {t('retenue', 'lot_chargement', 'Lecture des lignes à envoyer…')}
                        </p>
                    )}

                    {erreurChargement && (
                        <Alerte>
                            <span>{erreurChargement}</span>
                            <Button variant="outline" size="sm" className="w-fit" onClick={() => void charger()}>
                                <RefreshCcw className="mr-2 h-4 w-4" />
                                {t('retenue', 'import_reessayer', 'Réessayer')}
                            </Button>
                        </Alerte>
                    )}

                    {donnees && lignes.length === 0 && !etatLot && (
                        <p className="text-muted-foreground py-4 text-sm">
                            {t('retenue', 'lot_vide', 'Aucune ligne importée n’attend d’être envoyée.')}
                        </p>
                    )}

                    {progression && etatLot && (
                        <div className="flex flex-col gap-1.5">
                            <div className="flex items-center justify-between text-sm">
                                <span className="font-medium">
                                    {remplir(t('retenue', 'lot_progression', ':n / :total envoyée(s)'), {
                                        n: progression.envoyees,
                                        total: progression.total,
                                    })}
                                </span>
                                {enCours && <Spinner size="sm" />}
                            </div>
                            <progress
                                value={progression.envoyees}
                                max={Math.max(progression.total, 1)}
                                className="[&::-moz-progress-bar]:bg-primary [&::-webkit-progress-bar]:bg-muted [&::-webkit-progress-value]:bg-primary h-2 w-full overflow-hidden rounded-full [&::-webkit-progress-bar]:rounded-full [&::-webkit-progress-value]:rounded-full"
                            />
                        </div>
                    )}

                    {etatLot?.statut === 'arrete' && etatLot.erreur && (
                        <div className="flex gap-2 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-900">
                            <XCircle className="mt-0.5 h-4 w-4 shrink-0" />
                            <div className="flex min-w-0 flex-1 flex-col gap-1">
                                <span className="font-medium">
                                    {remplir(t('retenue', 'lot_arret_erreur', 'Envoi arrêté sur « :nom » :'), {
                                        nom: parId.get(etatLot.erreur.ligneId)?.beneficiaire_nom ?? `#${etatLot.erreur.ligneId}`,
                                    })}
                                </span>
                                <span>{etatLot.erreur.message}</span>
                                {/* Écartée par le serveur : elle ne partira jamais par
                                    ce lot, « Reprendre » passe à la suivante. Erreur
                                    ordinaire : « Reprendre » la retente, « Passer »
                                    l'écarte à la main. */}
                                {etatLot.lignes[etatLot.erreur.ligneId]?.etat === 'ecartee' ? (
                                    <span className="text-xs">
                                        {t(
                                            'retenue',
                                            'lot_arret_ecartee_aide',
                                            'Cette ligne ne peut pas partir par cet envoi : elle est écartée et ne sera pas renvoyée d’ici — vérifiez son état sur l’écran. « Reprendre » continue avec les lignes suivantes.',
                                        )}
                                    </span>
                                ) : (
                                    <Button
                                        variant="outline"
                                        size="sm"
                                        className="w-fit border-red-200 bg-white text-red-900 hover:bg-red-100"
                                        onClick={() => void passer()}
                                        disabled={creationLot}
                                    >
                                        <SkipForward className="mr-1.5 h-3.5 w-3.5" />
                                        {t('retenue', 'lot_passer', 'Passer cette ligne')}
                                    </Button>
                                )}
                            </div>
                        </div>
                    )}

                    {etatLot?.statut === 'arrete' && bilan && (
                        <p className="text-muted-foreground text-xs">
                            {remplir(
                                t(
                                    'retenue',
                                    'lot_bilan',
                                    'Envoyées : :envoyees · écartées : :ecartees · en erreur : :erreurs · restantes : :restantes',
                                ),
                                {
                                    envoyees: bilan.envoyees,
                                    ecartees: bilan.ecartees,
                                    erreurs: bilan.erreurs,
                                    restantes: bilan.enAttente,
                                },
                            )}
                        </p>
                    )}

                    {etatLot?.statut === 'arrete' && !etatLot.erreur && (
                        <p className="text-muted-foreground text-sm">
                            {t('retenue', 'lot_arret_demande', 'Envoi interrompu : les lignes restantes ne sont pas parties.')}
                        </p>
                    )}

                    {etatLot?.statut === 'termine' && progression && (
                        <div className="flex gap-2 rounded-md border border-emerald-200 bg-emerald-50 px-3 py-2 text-sm text-emerald-900">
                            <CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0" />
                            <span>
                                {remplir(
                                    t(
                                        'retenue',
                                        'lot_fini',
                                        ':n ligne(s) envoyée(s), :m écartée(s). Le verdict de la DGI s’affichera sur chaque ligne de l’écran.',
                                    ),
                                    { n: progression.envoyees, m: bilan?.ecartees ?? 0 },
                                )}
                            </span>
                        </div>
                    )}

                    {erreurLot && <Alerte>{erreurLot}</Alerte>}

                    {lignesAffichees.length > 0 && (
                        <ListeLignes
                            lignes={lignesAffichees}
                            etatLot={etatLot}
                            decochees={decochees}
                            onBasculer={basculer}
                            selectionFigee={etatLot !== null || creationLot}
                        />
                    )}

                    {bloquees.length > 0 && !etatLot && <ListeBloquees bloquees={bloquees} />}

                    {(donnees?.lignes.length ?? 0) > 0 && <LegendeCouleurs className="rounded-md border px-3 py-2" />}

                    {donnees && lignes.length > 0 && !etatLot && (
                        <label className="flex cursor-pointer items-start gap-2 rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-900">
                            <Checkbox className="mt-0.5" checked={verifie} onCheckedChange={(v) => setVerifie(v === true)} />
                            <span>
                                {remplir(
                                    t('retenue', 'lot_verifie', 'J’ai vérifié ces :n lignes. Chaque envoi est une déclaration réelle à la DGI.'),
                                    { n: choisies.length },
                                )}
                            </span>
                        </label>
                    )}
                </div>

                <DialogFooter className="gap-2 sm:gap-2">
                    <Button variant="outline" onClick={() => changerOuverture(false)}>
                        {etatLot?.statut === 'termine' ? t('retenue', 'import_fermer', 'Fermer') : t('retenue', 'cancel', 'Annuler')}
                    </Button>

                    {!etatLot && (
                        <Button onClick={() => void envoyer()} disabled={!verifie || choisies.length === 0 || creationLot}>
                            {creationLot ? <Spinner size="sm" className="mr-2 text-current" /> : <Send className="mr-2 h-4 w-4" />}
                            {remplir(t('retenue', 'lot_envoyer', 'Envoyer :n lignes'), { n: choisies.length })}
                        </Button>
                    )}

                    {etatLot?.statut === 'arrete' && (
                        <Button onClick={() => void reprendre()} disabled={creationLot}>
                            {creationLot ? <Spinner size="sm" className="mr-2 text-current" /> : <RefreshCcw className="mr-2 h-4 w-4" />}
                            {t('retenue', 'lot_reprendre', 'Reprendre')}
                        </Button>
                    )}
                </DialogFooter>
            </DialogContent>
        </Dialog>
    );
}

function Alerte({ children }: { children: React.ReactNode }) {
    return (
        <div className="flex gap-2 rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-900">
            <AlertTriangle className="mt-0.5 h-4 w-4 shrink-0" />
            <div className="flex min-w-0 flex-col gap-2">{children}</div>
        </div>
    );
}

/** Les lignes — à choisir avant l'envoi, à suivre pendant. Tableau ≥ 640 px, cartes en dessous. */
function ListeLignes({
    lignes,
    etatLot,
    decochees,
    onBasculer,
    selectionFigee,
}: {
    lignes: LigneAEnvoyer[];
    etatLot: EtatLot | null;
    decochees: ReadonlySet<number>;
    onBasculer: (id: number) => void;
    selectionFigee: boolean;
}) {
    const { t } = useTranslation();

    /** Avant l'envoi, la case ; pendant, l'icône de l'état de la ligne. */
    const caseOuIcone = (ligne: LigneAEnvoyer) =>
        etatLot ? (
            <IconeEtat resultat={etatLot.lignes[ligne.id]} />
        ) : (
            <Checkbox
                checked={!decochees.has(ligne.id)}
                disabled={selectionFigee}
                onCheckedChange={() => onBasculer(ligne.id)}
                aria-label={remplir(t('retenue', 'lot_case_ligne', 'Envoyer la ligne de :nom'), { nom: ligne.beneficiaire_nom })}
            />
        );

    return (
        <>
            <div className="hidden rounded-md border sm:block">
                <Table className="text-sm">
                    <TableHeader>
                        <TableRow>
                            <TableHead className="w-8">
                                <span className="sr-only">{t('retenue', 'lot_col_envoyer', 'Envoyer')}</span>
                            </TableHead>
                            <TableHead>{t('retenue', 'lot_col_beneficiaire', 'Bénéficiaire')}</TableHead>
                            <TableHead>{t('retenue', 'import_col_identifiant', 'Identifiant')}</TableHead>
                            <TableHead>{t('retenue', 'lot_col_mois', 'Mois')}</TableHead>
                            <TableHead className="text-right">{t('retenue', 'import_col_brut', 'Brut')}</TableHead>
                            <TableHead className="text-right">{t('retenue', 'import_col_retenue', 'Retenue')}</TableHead>
                            <TableHead>{t('retenue', 'lot_col_date', 'Date de paiement')}</TableHead>
                            <TableHead>{t('retenue', 'lot_col_acte', 'Acte')}</TableHead>
                            {etatLot && <TableHead>{t('retenue', 'lot_col_etat', 'Envoi')}</TableHead>}
                        </TableRow>
                    </TableHeader>
                    <TableBody>
                        {lignes.map((ligne) => (
                            <TableRow key={ligne.id} className={cn(etatLot?.courante === ligne.id && 'bg-sky-50/60')}>
                                <TableCell className="align-top">{caseOuIcone(ligne)}</TableCell>
                                <TableCell className="max-w-[220px] align-top font-medium whitespace-normal">{ligne.beneficiaire_nom}</TableCell>
                                <TableCell className="align-top font-mono text-xs">{ligne.identifiant ?? '—'}</TableCell>
                                <TableCell className="align-top whitespace-nowrap">{libelleMois(ligne.mois)}</TableCell>
                                <TableCell className="text-right align-top tabular-nums">{fmtMillimes(ligne.montant_brut)}</TableCell>
                                <TableCell className="text-right align-top tabular-nums">{fmtMillimes(ligne.montant_retenue)}</TableCell>
                                <TableCell className="align-top tabular-nums">{fmtDateFr(ligne.date_paiement) || '—'}</TableCell>
                                <TableCell className="align-top">
                                    <BadgeActe acte={ligne.acte} />
                                </TableCell>
                                {etatLot && (
                                    <TableCell className="max-w-[240px] align-top whitespace-normal">
                                        <EtatLigne resultat={etatLot.lignes[ligne.id]} />
                                    </TableCell>
                                )}
                            </TableRow>
                        ))}
                    </TableBody>
                </Table>
            </div>

            <ul className="divide-y rounded-md border sm:hidden">
                {lignes.map((ligne) => (
                    <li key={ligne.id} className={cn('flex gap-3 px-3 py-2.5', etatLot?.courante === ligne.id && 'bg-sky-50/60')}>
                        <div className="w-4 shrink-0 pt-0.5">{caseOuIcone(ligne)}</div>
                        <div className="flex min-w-0 flex-1 flex-col gap-1">
                            <div className="flex items-start justify-between gap-2">
                                <span className="text-sm font-medium break-words">{ligne.beneficiaire_nom}</span>
                                <BadgeActe acte={ligne.acte} />
                            </div>
                            <span className="text-muted-foreground text-xs">
                                <span className="font-mono">{ligne.identifiant ?? '—'}</span> · {libelleMois(ligne.mois)} ·{' '}
                                {fmtDateFr(ligne.date_paiement) || '—'}
                            </span>
                            <span className="text-xs">
                                {t('retenue', 'import_col_brut', 'Brut')} <span className="tabular-nums">{fmtMillimes(ligne.montant_brut)}</span> ·{' '}
                                {t('retenue', 'import_col_retenue', 'Retenue')}{' '}
                                <span className="tabular-nums">{fmtMillimes(ligne.montant_retenue)}</span>
                            </span>
                            {etatLot && <EtatLigne resultat={etatLot.lignes[ligne.id]} />}
                        </div>
                    </li>
                ))}
            </ul>
        </>
    );
}

function BadgeActe({ acte }: { acte: LigneAEnvoyer['acte'] }) {
    const { t } = useTranslation();

    return acte === 'rectificatif' ? (
        <Badge variant="outline" className="border-amber-200 bg-amber-50 text-amber-800">
            {t('retenue', 'lot_acte_rectificatif', 'Rectificatif')}
        </Badge>
    ) : (
        <Badge variant="outline">{t('retenue', 'lot_acte_initial', 'Initial')}</Badge>
    );
}

function IconeEtat({ resultat }: { resultat: ResultatLigneLot | undefined }) {
    switch (resultat?.etat) {
        case 'en_cours':
            return <Spinner size="sm" />;
        case 'envoyee':
            return <CheckCircle2 className="h-4 w-4 text-emerald-600" />;
        case 'erreur':
            return <XCircle className="h-4 w-4 text-red-600" />;
        case 'ecartee':
            return <Ban className="h-4 w-4 text-amber-600" />;
        default:
            return <Clock className="text-muted-foreground h-4 w-4" />;
    }
}

/** L'état d'une ligne pendant l'envoi : en attente, en cours, envoyée (+ référence), erreur (+ message). */
function EtatLigne({ resultat }: { resultat: ResultatLigneLot | undefined }) {
    const { t } = useTranslation();

    switch (resultat?.etat) {
        case 'en_cours':
            return <span className="text-sky-800">{t('retenue', 'lot_etat_en_cours', 'En cours…')}</span>;
        case 'envoyee':
            return (
                <span className="flex flex-col text-emerald-800">
                    <span>{t('retenue', 'lot_etat_envoyee', 'Envoyée')}</span>
                    {resultat.reference && <span className="font-mono text-[11px]">{resultat.reference}</span>}
                </span>
            );
        case 'erreur':
            return (
                <span className="flex flex-col text-red-800">
                    <span>{t('retenue', 'lot_etat_erreur', 'Erreur')}</span>
                    {resultat.message && <span className="text-xs">{resultat.message}</span>}
                </span>
            );
        case 'ecartee':
            return (
                <span className="text-amber-800">
                    {remplir(t('retenue', 'lot_etat_ecartee', 'Écartée : :message'), { message: resultat.message ?? '—' })}
                </span>
            );
        default:
            return <span className="text-muted-foreground">{t('retenue', 'lot_etat_en_attente', 'En attente')}</span>;
    }
}

/** Les lignes importées qui ne peuvent pas partir — et pourquoi. Repliées : elles ne se cochent pas. */
function ListeBloquees({ bloquees }: { bloquees: AEnvoyer['bloquees'] }) {
    const { t } = useTranslation();
    const [ouvert, setOuvert] = React.useState(false);

    return (
        <Collapsible open={ouvert} onOpenChange={setOuvert} className="rounded-md border border-amber-200">
            <CollapsibleTrigger className="flex w-full items-center gap-1.5 px-3 py-2 text-left text-sm text-amber-900">
                <ChevronRight className={cn('h-4 w-4 shrink-0 transition-transform', ouvert && 'rotate-90')} />
                {remplir(t('retenue', 'lot_bloquees_titre', ':n ligne(s) importée(s) ne peuvent pas partir tant qu’elles ne sont pas corrigées'), {
                    n: bloquees.length,
                })}
            </CollapsibleTrigger>
            <CollapsibleContent>
                <ul className="border-t border-amber-200 text-sm">
                    {bloquees.map((b) => (
                        <li key={b.id} className="flex flex-col gap-0.5 border-b border-amber-100 px-3 py-1.5 last:border-b-0">
                            <span className="font-medium">
                                {b.beneficiaire_nom} <span className="text-muted-foreground font-normal">· {libelleMois(b.mois)}</span>
                            </span>
                            <ul className="text-muted-foreground list-disc pl-4 text-xs">
                                {b.motifs.map((motif, i) => (
                                    <li key={i}>{motif}</li>
                                ))}
                            </ul>
                        </li>
                    ))}
                </ul>
            </CollapsibleContent>
        </Collapsible>
    );
}
