import { FrDateInput } from '@/components/fr-date-input';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from '@/components/ui/select';
import SearchableSelect from '@/components/ui/select-search';
import { Textarea } from '@/components/ui/textarea';
import { useTranslation } from '@/hooks/use-translation';
import { useForm, usePage } from '@inertiajs/react';
import { AlertTriangle, CheckCircle2, Plus, Upload } from 'lucide-react';
import * as React from 'react';
import { OwnerFicheDialog } from '../fournisseurs/owner-fiche-dialog';
import { SupplierRegistreDialog } from '../fournisseurs/supplier-registre-dialog';
import { apercuCertificat, fmtMillimes, fmtTaux } from './format';
import { IdentiteDialog } from './identite-dialog';
import { tauxCorrespondant } from './taux';
import type { BeneficiaireOption, OperationTejOption, OwnerOption, RetenueLigne, TauxOption, TypeBeneficiaire } from './types';

/**
 * Valeur sentinelle du <Select> de taux : « Autre » ⇒ saisie manuelle.
 * Aucun code de `config('retenue.taux')` ne vaut « autre ».
 */
const TAUX_AUTRE = 'autre';

const TYPES: readonly TypeBeneficiaire[] = ['proprietaire', 'fournisseur'];

/**
 * Identité fiscale affichée à côté du nom, préfixée par sa nature.
 *
 * Le matricule prime sur le CIN quand les deux existent : c'est lui que TEJ
 * attend pour une personne morale, et c'est celui qui partira dans le fichier.
 * Sans identifiant, on le DIT — une fiche muette ici est une fiche qui bloquera
 * la clôture, autant le voir au moment de choisir plutôt qu'au moment de déposer.
 */
function identifiantLisible(matricule: string | null, cin: string | null): string {
    if (matricule) return `MF ${matricule}`;
    if (cin) return `CIN ${cin}`;

    return 'sans identité fiscale';
}

/** Un montant calculé, libellé au-dessus — la disposition de l'écran de TEJ. */
function Montant({ libelle, valeur, accent = false }: { libelle: string; valeur: number; accent?: boolean }) {
    return (
        <div className="flex flex-col">
            <span className="text-muted-foreground text-[11px] leading-tight">{libelle}</span>
            <span className={`text-sm tabular-nums ${accent ? 'font-semibold' : ''}`}>{fmtMillimes(valeur)}</span>
        </div>
    );
}

/** Nom à gauche, identité fiscale à droite — c'est elle qui distingue deux homonymes. */
function rendreBeneficiaire(item: { name: string; [key: string]: unknown }) {
    const identifiant = String(item.identifiant ?? '');
    const manquant = identifiant === 'sans identité fiscale';

    return (
        <>
            <span className="min-w-0 flex-1 truncate font-medium">{item.name}</span>
            <span className={`ml-2 shrink-0 text-xs ${manquant ? 'text-amber-600' : 'text-muted-foreground'}`}>{identifiant}</span>
        </>
    );
}

/**
 * Regroupe la nomenclature par type d'opération.
 *
 * Une vingtaine d'opérations, dont certains libellés font 400 caractères : à
 * plat, la liste est illisible. Les groupes reprennent la taxonomie de la DGI
 * (« Loyers », « Acquisitions des marchandises… »), dans l'ordre rendu par le
 * serveur — qui les trie déjà par identifiant de type.
 */
function groupesOperations(operations: OperationTejOption[]): { nom: string; operations: OperationTejOption[] }[] {
    const groupes: { nom: string; operations: OperationTejOption[] }[] = [];

    for (const operation of operations) {
        const dernier = groupes[groupes.length - 1];
        if (dernier?.nom === operation.groupe) {
            dernier.operations.push(operation);
        } else {
            groupes.push({ nom: operation.groupe, operations: [operation] });
        }
    }

    return groupes;
}

interface Props {
    open: boolean;
    onOpenChange: (open: boolean) => void;
    declarationId: number;
    /** Ligne à modifier ; `null` = création. */
    ligne: RetenueLigne | null;
    /** Taux proposés — les mêmes pour un propriétaire et pour un fournisseur. */
    taux: TauxOption[];
    proprietaires: OwnerOption[];
    fournisseurs: BeneficiaireOption[];
    /** Nomenclature TEJ — vide tant que `tej:sync-referentiels` n'a pas tourné. */
    operationsTej: OperationTejOption[];
    /** `fournisseur.create`. Faux ⇒ pas de bouton d'ajout dans la liste. */
    peutCreerFournisseur?: boolean;
    /** `retenue_cloture.declare`. Faux ⇒ pas de raccourci de dépôt après création. */
    peutDeposer?: boolean;
    /**
     * Raccourci « Déposer sur TEJ » de la dernière étape.
     *
     * Reçoit l'id ET le nom de la ligne qui vient d'être créée, et doit ouvrir
     * l'APERÇU du dépôt — jamais l'envoi. Ce dialogue-ci se ferme juste avant.
     *
     * Le nom voyage avec l'id parce que l'écran ne peut pas le retrouver : ses
     * listes de lignes sont filtrées côté serveur.
     */
    onDeposerTej?: (ligneId: number, nom: string) => void;
}

/**
 * Saisie d'une ligne — création ou modification.
 *
 * En création, le type de bénéficiaire est demandé en premier : il décide du
 * sélecteur affiché ensuite. En modification il est figé, comme l'identité.
 *
 * Le champ « Montant » pilote `montant_brut` directement : c'est le montant
 * déclaré, la seule vérité. `montant_brut_calcule` reste la référence du
 * serveur pour signaler un écart, il n'est jamais saisi ici. L'aperçu de
 * retenue affiché est une aide à la saisie ; la valeur qui fait foi est celle
 * renvoyée par le serveur.
 */
export function LineDialog({
    open,
    onOpenChange,
    declarationId,
    ligne,
    taux,
    proprietaires,
    fournisseurs,
    operationsTej,
    peutCreerFournisseur = false,
    peutDeposer = false,
    onDeposerTej,
}: Props) {
    const { t } = useTranslation();
    const isEdit = ligne !== null;

    const form = useForm({
        beneficiaire_type: '' as TypeBeneficiaire | '',
        owner_user_id: '',
        beneficiaire_id: '',
        montant_brut: '',
        // Code du taux choisi, ou TAUX_AUTRE pour un taux saisi à la main.
        taux_code: '',
        taux_personnalise: '',
        facture_date: '',
        note: '',
        // Champs du fichier de dépôt TEJ. Tous facultatifs à la saisie : une
        // ligne incomplète s'enregistre et se complète. C'est le constructeur du
        // fichier qui refuse au dépôt, en nommant la ligne.
        montant_ht: '',
        taux_tva: '',
        tej_operation_code: '',
        annee_facturation: '',
        date_paiement: '',
        cnpc: false as boolean,
        prise_en_charge: false as boolean,
    });
    const { data, setData, processing, clearErrors } = form;
    // Le serveur peut renvoyer une clé absente du formulaire local : on lit les
    // erreurs par nom plutôt que par `keyof`.
    const errors = form.errors as Record<string, string | undefined>;

    const type = data.beneficiaire_type;
    const estPersonnalise = data.taux_code === TAUX_AUTRE;

    // Le dialogue reste monté en permanence (évite les fuites de body-lock
    // Radix) : on ré-hydrate les champs à chaque ouverture / changement de ligne.
    /**
     * Id de la ligne qui vient d'être créée — non nul ⇒ DERNIÈRE ÉTAPE.
     *
     * Le formulaire ne se ferme plus tout seul après une création : il laisse
     * la main sur « Envoyer à TEJ », qui évite d'aller rouvrir le menu de la
     * ligne dans le tableau. En modification, rien ne change : fermeture directe.
     */
    const [creee, setCreee] = React.useState<{ id: number; nom: string } | null>(null);

    React.useEffect(() => {
        if (!open) return;
        setCreee(null);
        clearErrors();
        // Une ligne ne porte que la valeur et le libellé de son taux : on
        // retrouve l'option correspondante, sinon c'est un taux personnalisé.
        const optionTaux = ligne ? tauxCorrespondant(taux, ligne) : null;
        setData({
            beneficiaire_type: ligne?.beneficiaire_type ?? '',
            // L'identité est figée en modification : ces deux sélecteurs ne
            // servent qu'à la création, inutile de les ré-hydrater.
            owner_user_id: '',
            beneficiaire_id: '',
            montant_brut: ligne ? String(ligne.montant_brut) : '',
            taux_code: ligne ? (optionTaux?.code ?? TAUX_AUTRE) : '',
            taux_personnalise: ligne && !optionTaux ? String(ligne.taux) : '',
            facture_date: ligne?.facture_date ?? '',
            note: ligne?.note ?? '',
            montant_ht: ligne && ligne.taux_tva > 0 ? String(ligne.montant_ht) : '',
            taux_tva: ligne && ligne.taux_tva > 0 ? String(ligne.taux_tva) : '',
            tej_operation_code: ligne?.tej_operation_code ?? '',
            // L'exercice par défaut est l'année en cours : c'est le cas de
            // presque toutes les saisies, et le portail de TEJ le pré-remplit
            // pareil. Il reste modifiable — une facture de N payée en N+1 les dissocie.
            annee_facturation: String(ligne?.annee_facturation ?? new Date().getFullYear()),
            date_paiement: ligne?.date_paiement ?? '',
            cnpc: ligne?.cnpc ?? false,
            prise_en_charge: ligne?.prise_en_charge ?? false,
        });
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [open, ligne?.id]);

    const tauxSelectionne = taux.find((option) => option.code === data.taux_code) ?? null;

    const valeurTaux = estPersonnalise ? Number(data.taux_personnalise.replace(',', '.')) : tauxSelectionne ? Number(tauxSelectionne.taux) : null;
    const seuilTaux = estPersonnalise ? null : (tauxSelectionne?.seuil_min_ttc ?? null);

    const montantSaisi = Number(data.montant_brut.replace(',', '.'));
    const montantApercu = Number.isFinite(montantSaisi) && data.montant_brut !== '' ? montantSaisi : 0;
    const nombreSaisi = (valeur: string) => {
        const n = Number(valeur.replace(',', '.'));

        return valeur.trim() !== '' && Number.isFinite(n) ? n : 0;
    };

    const apercu =
        valeurTaux !== null && Number.isFinite(valeurTaux)
            ? apercuCertificat({
                  montantTtc: montantApercu,
                  montantHt: nombreSaisi(data.montant_ht),
                  tauxTva: nombreSaisi(data.taux_tva),
                  tauxRs: valeurTaux,
                  seuilMinTtc: seuilTaux,
              })
            : null;

    const proprietaireItems = React.useMemo(
        () =>
            proprietaires.map((owner) => ({
                id: owner.id,
                name: owner.nom,
                searchValue: `${owner.matricule_fiscal ?? ''} ${owner.cin ?? ''}`,
                identifiant: identifiantLisible(owner.matricule_fiscal, owner.cin),
            })),
        [proprietaires],
    );

    const fournisseurItems = React.useMemo(
        () =>
            fournisseurs.map((beneficiaire) => ({
                id: beneficiaire.id,
                name: beneficiaire.raison_sociale,
                searchValue: `${beneficiaire.matricule_fiscal ?? ''} ${beneficiaire.cin ?? ''}`,
                identifiant: identifiantLisible(beneficiaire.matricule_fiscal, beneficiaire.cin),
            })),
        [fournisseurs],
    );

    /** Changer de type repart d'un bénéficiaire vide : les deux listes sont disjointes. */
    const choisirType = (value: TypeBeneficiaire) => {
        setData((previous) => ({ ...previous, beneficiaire_type: value, owner_user_id: '', beneficiaire_id: '' }));
    };

    /*
     * ═══════════════════════════════════════════════════════════════════════
     * COMPLÉTER SANS QUITTER LA SAISIE
     * ═══════════════════════════════════════════════════════════════════════
     * `creation` : l'ajout d'un fournisseur, empilé PAR-DESSUS ce formulaire.
     * Le fermer ne ferme pas la ligne en cours — c'est tout l'intérêt : on crée
     * le tiers qui manquait et on revient à sa saisie.
     *
     * ⚠️ AUCUNE ÉTAPE DE CHOIX ICI, et ce n'est pas un raccourci : sur ce
     * parc, un « fournisseur » de la retenue EST une société. Le type de
     * bénéficiaire a déjà été tranché au-dessus, par la ligne elle-même — une
     * personne physique s'y saisit comme PROPRIÉTAIRE. Redemander « société ou
     * personne physique ? » posait une question dont la réponse était déjà
     * donnée, et ouvrait la porte à créer un tiers au CIN là où il fallait un
     * propriétaire : le doublon exact qu'on cherche à empêcher.
     *
     * `correction` : la fiche fournisseur dont l'identité est incomplète.
     * `ownerACompleter` : le COMPTE propriétaire dont l'identité est incomplète.
     */
    const [creation, setCreation] = React.useState<boolean>(false);
    const [correction, setCorrection] = React.useState<BeneficiaireOption | null>(null);
    const [ownerACompleter, setOwnerACompleter] = React.useState<number | null>(null);

    /*
     * SÉLECTION AUTOMATIQUE du fournisseur qu'on vient de créer.
     *
     * `BeneficiaireController::store()` renvoie son id dans le flash. Sans lui,
     * l'utilisateur retrouverait la liste rafraîchie mais son champ vide, et
     * devrait rechercher à la main la fiche qu'il vient de saisir.
     *
     * La garde sur `fournisseurs` n'est pas cosmétique : le flash arrive avec la
     * réponse Inertia, la prop aussi, mais un id qui ne figure PAS encore dans
     * la liste (fiche inactive, périmètre filtré) laisserait le sélecteur sur
     * une valeur qu'il ne sait pas afficher — un champ qui paraît vide alors
     * qu'il porte une valeur, et le pire des deux mondes.
     */
    const flash = usePage<{ flash?: { fiche_id?: number } }>().props.flash;
    React.useEffect(() => {
        const id = flash?.fiche_id;
        if (id === undefined || !open || isEdit) return;
        if (!fournisseurs.some((f) => f.id === id)) return;
        setData((previous) => ({ ...previous, beneficiaire_type: 'fournisseur', beneficiaire_id: String(id) }));
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [flash?.fiche_id, fournisseurs]);

    /**
     * Ce qui manque au bénéficiaire choisi, et où le corriger.
     *
     * `href` ⇒ la donnée vit AILLEURS et un lien y mène (le compte d'un
     * propriétaire) ; `fiche` ⇒ elle se corrige ici même.
     */
    const manque = React.useMemo((): { libelle: string; ownerId?: number; fiche?: BeneficiaireOption } | null => {
        if (isEdit) return null; // en modification, l'identité est figée sur la ligne

        if (type === 'proprietaire' && data.owner_user_id !== '') {
            const owner = proprietaires.find((o) => String(o.id) === data.owner_user_id);
            if (owner && !owner.matricule_fiscal && !owner.cin) {
                return {
                    libelle: t('retenue', 'manque_identite_proprietaire', 'Ce propriétaire n’a ni matricule fiscal ni CIN.'),
                    ownerId: owner.id,
                };
            }
            return null;
        }

        if (type === 'fournisseur' && data.beneficiaire_id !== '') {
            const fiche = fournisseurs.find((f) => String(f.id) === data.beneficiaire_id);
            if (fiche && !fiche.matricule_fiscal && !fiche.cin) {
                return {
                    libelle: t('retenue', 'manque_identite_fournisseur', 'Ce fournisseur n’a ni matricule fiscal ni CIN.'),
                    fiche,
                };
            }
        }

        return null;
    }, [isEdit, type, data.owner_user_id, data.beneficiaire_id, proprietaires, fournisseurs, t]);

    /** Le serveur attend `taux_code` OU `taux_personnalise`, jamais les deux. */
    const champsTaux = (current: typeof data): Record<string, string | number | null> => {
        if (current.taux_code === TAUX_AUTRE) {
            const valeur = current.taux_personnalise.replace(',', '.');
            return { taux_personnalise: valeur.trim() === '' ? null : Number(valeur) };
        }
        return { taux_code: current.taux_code === '' ? null : current.taux_code };
    };

    /**
     * Champs du fichier de dépôt. Envoyés à chaque fois, y compris vides :
     * effacer une opération choisie par erreur doit être possible, et une clé
     * absente d'un PATCH `sometimes` ne serait tout simplement pas traitée.
     */
    const champsTej = (current: typeof data): Record<string, string | number | boolean | null> => {
        const nombre = (value: string) => (value.trim() === '' ? null : Number(value.replace(',', '.')));

        return {
            montant_ht: nombre(current.montant_ht),
            taux_tva: nombre(current.taux_tva),
            tej_operation_code: current.tej_operation_code === '' ? null : current.tej_operation_code,
            annee_facturation: nombre(current.annee_facturation),
            date_paiement: current.date_paiement === '' ? null : current.date_paiement,
            cnpc: current.cnpc,
            prise_en_charge: current.prise_en_charge,
        };
    };

    const submit = (event: React.FormEvent) => {
        event.preventDefault();

        const optionnel = (value: string) => (value.trim() === '' ? null : value.trim());
        // Un montant vide part en `null` : c'est le serveur qui refuse, on ne
        // fabrique pas un 0 silencieux à sa place.
        const montant = (value: string) => (value.trim() === '' ? null : Number(value.replace(',', '.')));

        if (isEdit && ligne) {
            form.transform((current) => ({
                montant_brut: montant(current.montant_brut),
                ...champsTaux(current),
                ...champsTej(current),
                facture_date: optionnel(current.facture_date),
                note: optionnel(current.note),
            }));
            form.patch(route('retenue.lignes.update', { id: declarationId, ligne: ligne.id }), {
                preserveScroll: true,
                onSuccess: () => onOpenChange(false),
            });
            return;
        }

        form.transform((current) => ({
            beneficiaire_type: current.beneficiaire_type,
            owner_user_id: current.beneficiaire_type === 'proprietaire' && current.owner_user_id !== '' ? Number(current.owner_user_id) : null,
            beneficiaire_id: current.beneficiaire_type === 'fournisseur' && current.beneficiaire_id !== '' ? Number(current.beneficiaire_id) : null,
            montant_brut: montant(current.montant_brut),
            ...champsTaux(current),
            ...champsTej(current),
            facture_date: optionnel(current.facture_date),
            note: optionnel(current.note),
        }));
        form.post(route('retenue.lignes.store', { id: declarationId }), {
            preserveScroll: true,
            onSuccess: (page) => {
                // `ligne_id` vient du flash de `storeLigne()`. Sans lui — ou
                // sans le droit de déposer — on garde le comportement d'avant :
                // le dialogue se ferme, point.
                const flash = page.props.flash as { ligne_id?: number; ligne_nom?: string } | undefined;

                if (!peutDeposer || typeof flash?.ligne_id !== 'number') {
                    onOpenChange(false);

                    return;
                }

                setCreee({ id: flash.ligne_id, nom: flash.ligne_nom ?? '' });
            },
        });
    };

    const libelleType =
        type === '' ? t('retenue', 'beneficiary_type') : t('retenue', type === 'proprietaire' ? 'type_proprietaire' : 'type_fournisseur');

    return (
        <Dialog open={open} onOpenChange={onOpenChange}>
            <DialogContent
                className="max-h-[90vh] overflow-y-auto sm:max-w-3xl"
                /**
                 * Un formulaire de saisie ne se ferme jamais sur un clic à côté :
                 * le calendrier de FrDateInput s'ouvre dans un portail, donc Radix
                 * interprétait le choix d'une date comme un clic « à l'extérieur »
                 * et vidait tout le formulaire. Échap et les boutons Annuler / ×
                 * restent les seules sorties.
                 */
                onInteractOutside={(e) => e.preventDefault()}
            >
                <DialogHeader>
                    <DialogTitle>
                        {creee !== null
                            ? t('retenue', 'ligne_creee', 'Ligne ajoutée')
                            : isEdit
                              ? t('retenue', 'edit_line')
                              : t('retenue', 'add_line')}
                    </DialogTitle>
                    <DialogDescription>
                        {creee !== null
                            ? t('retenue', 'ligne_creee_hint', 'Vous pouvez la déposer sur TEJ maintenant, ou plus tard depuis le menu de la ligne.')
                            : libelleType}
                    </DialogDescription>
                </DialogHeader>

                {/* ─────────────────────────────────────────────────────────────
                    DERNIÈRE ÉTAPE — le raccourci vers le dépôt.

                    Elle n'existe qu'en CRÉATION, et seulement pour qui a le
                    droit de déposer. Sa raison d'être : la ligne saisie devait
                    jusqu'ici être retrouvée dans le tableau, puis son menu ⋯
                    ouvert, pour partir chez la DGI.

                    ⚠️ « Déposer sur TEJ » n'envoie RIEN. Il ferme ce dialogue et
                    ouvre l'aperçu, qui montre le fichier et exige la case de
                    relecture — règle n°1 de `TejDeclarationService`. Raccourcir
                    le chemin ne veut pas dire retirer le filet.
                    ───────────────────────────────────────────────────────── */}
                {creee !== null ? (
                    <div className="flex flex-col gap-4">
                        <div className="flex items-start gap-2 rounded-md border border-emerald-300 bg-emerald-50 px-4 py-3 text-sm text-emerald-900">
                            <CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0" />
                            <span>{t('retenue', 'ligne_creee_corps', 'La ligne est enregistrée. Rien n’est encore parti vers la DGI.')}</span>
                        </div>

                        <DialogFooter>
                            <Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
                                {t('retenue', 'dismiss')}
                            </Button>
                            <Button
                                type="button"
                                onClick={() => {
                                    const cible = creee;
                                    onOpenChange(false);
                                    onDeposerTej?.(cible.id, cible.nom);
                                }}
                            >
                                <Upload className="mr-2 h-4 w-4" />
                                {t('retenue', 'tej_deposer', 'Déposer sur TEJ')}
                            </Button>
                        </DialogFooter>
                    </div>
                ) : (
                    <form onSubmit={submit} className="flex flex-col gap-4">
                        {/* Premier choix : propriétaire ou fournisseur — figé en modification. */}
                        <div className="flex flex-col gap-1.5">
                            <Label>{t('retenue', 'beneficiary_type')}</Label>
                            {isEdit ? (
                                <p className="bg-muted/40 rounded-md border px-3 py-2 text-sm font-medium">{libelleType}</p>
                            ) : (
                                <RadioGroup value={type} onValueChange={(value) => choisirType(value as TypeBeneficiaire)} className="grid-cols-2">
                                    {TYPES.map((option) => (
                                        <Label
                                            key={option}
                                            htmlFor={`retenue-type-${option}`}
                                            className="hover:bg-accent has-[[data-state=checked]]:border-primary flex cursor-pointer items-center gap-2 rounded-md border px-3 py-2 text-sm font-normal"
                                        >
                                            <RadioGroupItem id={`retenue-type-${option}`} value={option} />
                                            {t('retenue', option === 'proprietaire' ? 'type_proprietaire' : 'type_fournisseur')}
                                        </Label>
                                    ))}
                                </RadioGroup>
                            )}
                            <FieldError message={errors.beneficiaire_type} />
                        </div>

                        {type !== '' && (
                            <>
                                {/* Bénéficiaire — figé en modification : l'identité est gelée sur la ligne. */}
                                <div className="flex flex-col gap-1.5">
                                    <Label>{type === 'proprietaire' ? t('retenue', 'col_owner_name') : t('retenue', 'col_supplier')}</Label>
                                    {isEdit ? (
                                        <p className="bg-muted/40 rounded-md border px-3 py-2 text-sm font-medium">{ligne?.beneficiaire_nom}</p>
                                    ) : type === 'proprietaire' ? (
                                        <SearchableSelect
                                            value={data.owner_user_id}
                                            onValueChange={(value) => setData('owner_user_id', value)}
                                            items={proprietaireItems}
                                            renderItem={rendreBeneficiaire}
                                            placeholder={t('retenue', 'select_owner')}
                                            notFoundMessage={t('retenue', 'no_result')}
                                            className="bg-background text-foreground rounded-md"
                                        />
                                    ) : (
                                        <SearchableSelect
                                            value={data.beneficiaire_id}
                                            onValueChange={(value) => setData('beneficiaire_id', value)}
                                            items={fournisseurItems}
                                            renderItem={rendreBeneficiaire}
                                            placeholder={t('retenue', 'select_supplier')}
                                            notFoundMessage={t('retenue', 'no_result')}
                                            className="bg-background text-foreground rounded-md"
                                            /* « Le fournisseur n'existe pas encore » se constate DANS la
                                           liste, après avoir cherché. C'est donc là que l'action doit
                                           être, et non à côté du champ — d'où il faudrait refermer la
                                           liste pour l'atteindre. */
                                            footer={
                                                peutCreerFournisseur ? (
                                                    <button
                                                        type="button"
                                                        onClick={() => setCreation(true)}
                                                        className="hover:bg-accent hover:text-accent-foreground flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-left text-sm font-medium"
                                                    >
                                                        <Plus className="h-3.5 w-3.5 shrink-0" />
                                                        {t('retenue', 'ajouter_fournisseur', 'Ajouter un fournisseur')}
                                                    </button>
                                                ) : null
                                            }
                                        />
                                    )}

                                    {/* Ce qui manque à celui qu'on vient de choisir, et le geste
                                    pour le combler — ici, sans perdre la saisie en cours.
                                    Une identité absente ne se voit sinon qu'au moment de
                                    clôturer, quand il est trop tard pour la corriger de tête. */}
                                    {manque !== null && (
                                        <div className="flex items-center justify-between gap-2 rounded-md border border-amber-300 bg-amber-50 px-3 py-2 text-xs text-amber-900">
                                            <span className="flex items-center gap-1.5">
                                                <AlertTriangle className="h-3.5 w-3.5 shrink-0" />
                                                {manque.libelle}
                                            </span>
                                            {/* Un seul geste dans les deux cas, et il ouvre un
                                            formulaire PRÉ-REMPLI : le compte pour un
                                            propriétaire, la fiche pour un fournisseur. */}
                                            <Button
                                                type="button"
                                                variant="outline"
                                                size="sm"
                                                className="h-7 shrink-0 border-amber-400 bg-white text-xs"
                                                onClick={() =>
                                                    manque.ownerId ? setOwnerACompleter(manque.ownerId) : setCorrection(manque.fiche ?? null)
                                                }
                                            >
                                                {t('retenue', 'completer', 'Compléter')}
                                            </Button>
                                        </div>
                                    )}
                                    <FieldError message={errors.owner_user_id ?? errors.beneficiaire_id} />
                                </div>

                                <div className="flex flex-col gap-1.5">
                                    <Label htmlFor="retenue-montant">
                                        {type === 'fournisseur' ? t('retenue', 'col_gross_ttc') : t('retenue', 'col_gross')}
                                    </Label>
                                    <Input
                                        id="retenue-montant"
                                        type="number"
                                        step="0.001"
                                        min="0"
                                        inputMode="decimal"
                                        value={data.montant_brut}
                                        onChange={(e) => setData('montant_brut', e.target.value)}
                                        placeholder="0,000"
                                        className="tabular-nums"
                                    />
                                    <FieldError message={errors.montant_brut} />
                                </div>

                                {/* Taux : les mêmes pour les deux types, plus « Autre ». */}
                                <div className="flex flex-col gap-1.5">
                                    <Label>{t('retenue', 'col_rate')}</Label>
                                    <Select value={data.taux_code || undefined} onValueChange={(value) => setData('taux_code', value)}>
                                        <SelectTrigger>
                                            <SelectValue placeholder={t('retenue', 'select_rate')} />
                                        </SelectTrigger>
                                        <SelectContent>
                                            {taux.map((option) => (
                                                <SelectItem key={option.code} value={option.code}>
                                                    {fmtTaux(option.taux)} — {option.libelle}
                                                </SelectItem>
                                            ))}
                                            <SelectItem value={TAUX_AUTRE}>{t('retenue', 'rate_custom')}</SelectItem>
                                        </SelectContent>
                                    </Select>
                                    <FieldError message={errors.taux_code} />
                                </div>

                                {estPersonnalise && (
                                    <div className="flex flex-col gap-1.5">
                                        <Label htmlFor="retenue-taux-perso">{t('retenue', 'rate_value')}</Label>
                                        <Input
                                            id="retenue-taux-perso"
                                            type="number"
                                            step="0.01"
                                            min="0"
                                            max="100"
                                            inputMode="decimal"
                                            value={data.taux_personnalise}
                                            onChange={(e) => setData('taux_personnalise', e.target.value)}
                                            className="tabular-nums"
                                        />
                                        <FieldError message={errors.taux_personnalise} />
                                    </div>
                                )}

                                {/* Les quatre montants que TEJ affiche sur son propre écran,
                                calculés ici avec SA règle : troncature au millime, jamais
                                arrondi. Aide à la saisie — la valeur qui fait foi reste
                                celle que le serveur renvoie après enregistrement. */}
                                {apercu && (
                                    <div className="bg-muted/40 flex flex-col gap-2 rounded-md border px-3 py-2.5">
                                        <span className="text-muted-foreground text-xs">{t('retenue', 'preview_hint')}</span>
                                        <div className="grid grid-cols-2 gap-x-4 gap-y-1.5 sm:grid-cols-4">
                                            <Montant libelle="Montant TVA" valeur={apercu.tva} />
                                            <Montant libelle="Montant TTC" valeur={apercu.ttc} />
                                            <Montant libelle="Montant de la RS" valeur={apercu.retenue} accent />
                                            <Montant libelle="Net payé" valeur={apercu.net} accent />
                                        </div>
                                        {apercu.sousSeuil && <span className="text-xs text-amber-700">{t('retenue', 'rate_below_threshold')}</span>}
                                    </div>
                                )}

                                <div className="flex flex-col gap-3 rounded-md border border-dashed p-3">
                                    <div className="flex flex-wrap items-baseline justify-between gap-2">
                                        <p className="text-muted-foreground text-xs font-medium">Dépôt TEJ</p>
                                        {/* Le numéro du certificat chez le déclarant. Attribué
                                        à l'enregistrement, jamais saisi : c'est la clé par
                                        laquelle TEJ identifiera ce certificat pour le
                                        modifier ou l'annuler, elle ne doit pas bouger. */}
                                        <p className="text-muted-foreground text-xs">
                                            N° certificat :{' '}
                                            <span className="text-foreground font-medium tabular-nums">
                                                {ligne?.tej_reference ?? 'attribué à l’enregistrement'}
                                            </span>
                                        </p>
                                    </div>

                                    {/* HT + taux de TVA : renseignés, ils DÉTERMINENT le
                                    TTC (= la base de la retenue). Laissés vides, le
                                    montant saisi plus haut fait foi — c'est le cas
                                    des loyers, qui ne portent pas de TVA. */}
                                    <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
                                        <div className="flex flex-col gap-1.5">
                                            <Label htmlFor="retenue-ht">Montant HT</Label>
                                            <Input
                                                id="retenue-ht"
                                                inputMode="decimal"
                                                value={data.montant_ht}
                                                onChange={(e) => setData('montant_ht', e.target.value)}
                                                placeholder="1800.000"
                                            />
                                            <FieldError message={errors.montant_ht} />
                                        </div>
                                        <div className="flex flex-col gap-1.5">
                                            <Label htmlFor="retenue-tva">Taux TVA (%)</Label>
                                            <Input
                                                id="retenue-tva"
                                                inputMode="decimal"
                                                value={data.taux_tva}
                                                onChange={(e) => setData('taux_tva', e.target.value)}
                                                placeholder="19"
                                            />
                                            <FieldError message={errors.taux_tva} />
                                        </div>
                                    </div>

                                    <div className="flex flex-col gap-1.5">
                                        <Label>Opération TEJ</Label>
                                        {operationsTej.length === 0 ? (
                                            <p className="text-xs text-amber-700">
                                                Nomenclature vide — lancez « php artisan tej:sync-referentiels ».
                                            </p>
                                        ) : (
                                            <Select
                                                value={data.tej_operation_code || undefined}
                                                onValueChange={(value) => setData('tej_operation_code', value)}
                                            >
                                                <SelectTrigger>
                                                    <SelectValue placeholder="Choisir l'opération déclarée" />
                                                </SelectTrigger>
                                                <SelectContent className="max-w-[min(38rem,90vw)]">
                                                    {groupesOperations(operationsTej).map((groupe) => (
                                                        <SelectGroup key={groupe.nom}>
                                                            <SelectLabel>{groupe.nom}</SelectLabel>
                                                            {groupe.operations.map((operation) => (
                                                                <SelectItem key={operation.code} value={operation.code}>
                                                                    {/* Le CODE n'est pas affiché : « RS7_000002 » ne
                                                                    dit rien à personne, et c'est le libellé qui
                                                                    porte le sens. Il reste la valeur envoyée.
                                                                    `title` donne le texte entier au survol —
                                                                    certains libellés de la DGI font 400 signes. */}
                                                                    <span className="block max-w-[34rem] truncate" title={operation.label}>
                                                                        {operation.label}
                                                                    </span>
                                                                </SelectItem>
                                                            ))}
                                                        </SelectGroup>
                                                    ))}
                                                </SelectContent>
                                            </Select>
                                        )}
                                        <FieldError message={errors.tej_operation_code} />
                                    </div>

                                    <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
                                        <div className="flex flex-col gap-1.5">
                                            <Label htmlFor="retenue-exercice">Exercice de facturation</Label>
                                            <Input
                                                id="retenue-exercice"
                                                inputMode="numeric"
                                                value={data.annee_facturation}
                                                onChange={(e) => setData('annee_facturation', e.target.value)}
                                                placeholder="2026"
                                                maxLength={4}
                                            />
                                            <FieldError message={errors.annee_facturation} />
                                        </div>
                                        <div className="flex flex-col gap-1.5">
                                            {/* La date de paiement DÉCLENCHE le certificat, et
                                            son mois doit être celui de la période déclarée. */}
                                            <Label>Date de paiement</Label>
                                            <FrDateInput
                                                value={data.date_paiement}
                                                onChange={(iso) => setData('date_paiement', iso)}
                                                inputClassName="h-9"
                                            />
                                            <FieldError message={errors.date_paiement} />
                                        </div>
                                    </div>

                                    <div className="flex flex-wrap gap-x-6 gap-y-2">
                                        <label className="flex items-center gap-2 text-sm">
                                            <Checkbox
                                                checked={data.prise_en_charge}
                                                onCheckedChange={(valeur) => setData('prise_en_charge', valeur === true)}
                                            />
                                            Prise en charge
                                        </label>
                                        <label className="flex items-center gap-2 text-sm">
                                            <Checkbox checked={data.cnpc} onCheckedChange={(valeur) => setData('cnpc', valeur === true)} />
                                            Convention de non double imposition
                                        </label>
                                    </div>
                                </div>

                                <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
                                    <div className="flex flex-col gap-1.5">
                                        <Label>{t('retenue', 'invoice_date')}</Label>
                                        <FrDateInput
                                            value={data.facture_date}
                                            onChange={(iso) => setData('facture_date', iso)}
                                            inputClassName="h-9"
                                        />
                                        <FieldError message={errors.facture_date} />
                                    </div>
                                </div>

                                <div className="flex flex-col gap-1.5">
                                    <Label htmlFor="retenue-note">{t('retenue', 'note')}</Label>
                                    <Textarea
                                        id="retenue-note"
                                        value={data.note}
                                        onChange={(e) => setData('note', e.target.value)}
                                        rows={2}
                                        maxLength={1000}
                                    />
                                    <FieldError message={errors.note} />
                                </div>
                            </>
                        )}

                        <DialogFooter>
                            <Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
                                {t('retenue', 'cancel')}
                            </Button>
                            <Button type="submit" disabled={processing || type === ''}>
                                {t('retenue', 'save')}
                            </Button>
                        </DialogFooter>
                    </form>
                )}
            </DialogContent>

            {/* Empilés PAR-DESSUS la saisie, et non à sa place : les fermer
                ramène à la ligne en cours, montant et taux intacts.

                Le premier est le dialogue de /fournisseurs, pas une copie — un
                second formulaire de création aurait divergé du premier au
                premier changement de règle fiscale. Et c'est DIRECTEMENT celui
                du matricule : voir le commentaire de `creation` plus haut. */}
            <SupplierRegistreDialog open={creation} onOpenChange={setCreation} />

            <IdentiteDialog fiche={correction} onOpenChange={(value) => !value && setCorrection(null)} />

            {/* ⚠️ `onEnregistre` : sans lui, ce dialogue terminait par un
                `router.get('/fournisseurs')` qui QUITTAIT la retenue et
                emportait la ligne en cours de saisie — montant, taux et
                opération compris. Le rechargement n'a de sens que sur
                /fournisseurs, dont les filtres masqueraient la fiche neuve ;
                ici, `majProprietaire()` répond `back()` et les props se
                rafraîchissent d'elles-mêmes. */}
            <OwnerFicheDialog
                open={ownerACompleter !== null}
                onOpenChange={(value: boolean) => !value && setOwnerACompleter(null)}
                proprietaires={proprietaires}
                ownerId={ownerACompleter}
                onEnregistre={() => setOwnerACompleter(null)}
            />
        </Dialog>
    );
}

function FieldError({ message }: { message?: string }) {
    if (!message) return null;
    return <p className="text-destructive text-xs">{message}</p>;
}
