import { FrDateInput } from '@/components/fr-date-input';
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { correspondARecherche } from '@/components/ui/select-search-match';
import { useTranslation } from '@/hooks/use-translation';
import { router, useForm } from '@inertiajs/react';
import { Link2, Lock, Search } from 'lucide-react';
import * as React from 'react';
import { FicheCreeeStep } from './fiche-creee-step';
import { derniereDateNaissanceIso } from './tej';

/**
 * Un propriétaire du parc et l'état de sa fiche, tels que les sert
 * `RetenueLigneService::optionsProprietaires()` — une seule liste, pour les deux
 * écrans qui ouvrent ce dialogue.
 */
export interface OwnerOption {
    id: number;
    nom: string;
    matricule_fiscal: string | null;
    cin: string | null;
    type_personne: string | null;
    /** Identifie le COMPTE : montré, jamais saisissable. */
    email: string | null;
    telephone: string | null;
    adresse_fiscale: string | null;
    ville_fiscale: string | null;
    code_postal_fiscal: string | null;
    /** Non nul ⇒ la fiche existe déjà. Change le mot, pas le geste. */
    fiche_id: number | null;
    fiche_telephone: string | null;
    fiche_activite: string | null;
    /** `aaaa-mm-jj`. */
    fiche_date_naissance: string | null;
    /** Nombre de logements du parc — départage deux homonymes. */
    logements: number;
    /**
     * LA FICHE FOURNISSEUR QUI PORTE DÉJÀ SON IDENTIFIANT, et qui lui sera donc
     * rattachée à l'enregistrement. `null` quand il n'y en a pas — ou quand il a
     * déjà sa propre fiche, auquel cas il n'y a rien à rattacher.
     *
     * Calculée par `RetenueLigneService::optionsProprietaires()`, avec le même
     * ordre de repli que l'adoption réelle (matricule d'abord, CIN ensuite) :
     * annoncer autre chose que ce qui va se produire serait pire que se taire.
     */
    fiche_a_rattacher?: {
        id: number;
        /** La raison sociale de la fiche — souvent la graphie ARABE de la DGI. */
        nom: string;
        identifiant: string;
        /** La DGI la connaît : c'est celle-là qu'il faut garder. */
        connue_de_tej: boolean;
        /** À la corbeille : le rattachement la restaure. */
        corbeille: boolean;
    } | null;
}

interface Props {
    open: boolean;
    onOpenChange: (open: boolean) => void;
    /** Tous les propriétaires du parc. */
    proprietaires: OwnerOption[];
    /**
     * Propriétaire IMPOSÉ — depuis une ligne de retenue ou une fiche de la
     * liste. `null` ⇒ le dialogue le fait choisir.
     */
    ownerId?: number | null;
    /**
     * Raccourci de la dernière étape — ABSENT ⇒ pas de dernière étape.
     *
     * Ce dialogue est aussi monté DANS la saisie d'une ligne de retenue :
     * l'utilisateur y est au milieu d'autre chose, l'interrompre avec une étape
     * d'envoi y serait une nuisance. Seul /fournisseurs le passe.
     */
    onEnvoyerTej?: (ficheId: number) => void;
    /**
     * ⚠️ CE DIALOGUE RAMENAIT TOUT LE MONDE SUR /fournisseurs APRÈS AVOIR
     * ENREGISTRÉ, et c'était un défaut, pas une fonctionnalité.
     *
     * `rechargerLaListe()` fait un `router.get(fournisseurs.index)`. Sur
     * /fournisseurs c'est nécessaire — il faut LÂCHER les filtres en cours, qui
     * masqueraient la fiche qu'on vient d'enregistrer. Partout ailleurs, c'est
     * une éjection : depuis « Ajouter une ligne » de la retenue, cliquer
     * « Compléter » sur un propriétaire et enregistrer quittait l'écran ET
     * perdait la saisie en cours.
     *
     * Rien ne le justifiait : `majProprietaire()` répond `back()`, donc les
     * props de la page hôte se rafraîchissent toutes seules — la fiche neuve
     * apparaît dans ses listes sans aucune navigation.
     *
     * Passé ⇒ ce rappel remplace la navigation, et l'hôte décide de la suite.
     */
    onEnregistre?: () => void;
}

/**
 * La fiche fiscale d'un propriétaire — créée ou complétée, c'est le même écran.
 *
 * ═════════════════════════════════════════════════════════════════════════════
 * POURQUOI IL N'Y EN A QU'UN
 * ═════════════════════════════════════════════════════════════════════════════
 * Il y en a eu DEUX pendant une journée : un sur /fournisseurs qui savait
 * choisir un propriétaire mais pas corriger son identité — il se contentait
 * d'afficher « ce propriétaire n'a ni matricule ni CIN » sans offrir le moindre
 * champ — et un sur l'écran de retenue qui savait tout corriger mais ne savait
 * pas choisir. L'utilisateur tombait sur un cul-de-sac.
 *
 * La cause n'était pas l'écran : c'était d'avoir cru que « créer » et
 * « compléter » étaient deux opérations. `beneficiairePourProprietaire()` est
 * idempotent — il crée, adopte une fiche que TEJ connaît déjà, ou restaure
 * celle mise à la corbeille. Un seul geste, donc un seul formulaire et une
 * seule route (`fournisseurs.proprietaire`).
 *
 * ═════════════════════════════════════════════════════════════════════════════
 * TOUT CE QUE TEJ EXIGE, ET RIEN QUI SOIT ÉCRASÉ ENSUITE
 * ═════════════════════════════════════════════════════════════════════════════
 * `TejDeclarationXml` refuse le dépôt sans nom, adresse, activité, courriel ni
 * téléphone ; l'enregistrement du tiers exige en plus la date de naissance
 * quand c'est le CIN qui identifie. Tous sont ici, marqués.
 *
 * Le COURRIEL est la seule exception : il identifie le compte, il se corrige
 * dans /owners. S'il manque, on le DIT — c'est un refus de dépôt garanti.
 *
 * Le serveur range chaque champ où il doit vivre : l'identité sur le compte
 * (sinon le prochain rafraîchissement l'écrase), le téléphone, l'activité et la
 * date de naissance sur la fiche (`users` n'a pas ces colonnes).
 *
 * ⚠️ `onInteractOutside` neutralisé : le calendrier de `FrDateInput` s'ouvre
 * dans un portail Radix, donc HORS du dialogue. Sans ça, choisir une date ferme
 * le formulaire et efface la saisie (errors.md, 2026-08-04).
 */
export function OwnerFicheDialog({ open, onOpenChange, proprietaires, ownerId = null, onEnvoyerTej, onEnregistre }: Props) {
    const { t } = useTranslation();

    // Borne haute du calendrier : LA VEILLE. Le serveur valide `before:today` —
    // offrir aujourd'hui donnerait un jour qu'il rejette.
    const maxDateNaissance = React.useMemo(() => derniereDateNaissanceIso(), []);

    const form = useForm({
        owner_user_id: '',
        type_personne: 'physique',
        matricule_fiscal: '',
        cin: '',
        adresse_fiscale: '',
        ville_fiscale: '',
        code_postal_fiscal: '',
        telephone: '',
        activite: '',
        /** ISO `aaaa-mm-jj`, la forme qu'échange `FrDateInput`. */
        date_naissance: '',
    });
    const { data, setData, processing, clearErrors } = form;
    const errors = form.errors as Record<string, string | undefined>;

    /**
     * ═══════════════════════════════════════════════════════════════════════
     * ON RECHARGE LA LISTE SANS FILTRE
     * ═══════════════════════════════════════════════════════════════════════
     * Cette fonction FORÇAIT `perimetre=proprietaires`, parce que le défaut de
     * l'écran (« Tiers ») masquait les fiches de propriétaire : l'écran
     * annonçait « créée » et la liste restait identique. Depuis que le défaut
     * montre TOUT, il n'y a plus rien à forcer — et forcer un filtre après un
     * enregistrement était précisément le genre de surprise qu'on retire.
     *
     * `preserveState: false` : les filtres locaux doivent se réinitialiser
     * depuis les props, sinon un filtre laissé en place masquerait la fiche
     * qu'on vient d'enregistrer.
     *
     * `envoyer`, quand il est posé, demande à /fournisseurs d'ouvrir la
     * confirmation d'envoi sur cette fiche une fois la liste rechargée. Il
     * passe par l'URL et non par un état React parce que `preserveState: false`
     * REMONTE la page : tout état posé avant la visite serait perdu.
     */
    const rechargerLaListe = (envoyer?: number) => {
        // L'hôte a dit ce qu'il fallait faire : on ne navigue pas chez lui.
        if (onEnregistre) {
            onEnregistre();

            return;
        }

        router.get(route('fournisseurs.index'), { ...(envoyer !== undefined ? { envoyer } : {}) }, { preserveScroll: true, preserveState: false });
    };

    /** Fiche NEUVE et pas encore chez TEJ ⇒ dernière étape (« Envoyer à TEJ ? »). */
    const [creee, setCreee] = React.useState<{ id: number } | null>(null);

    const choisi = React.useMemo(() => proprietaires.find((o) => String(o.id) === data.owner_user_id) ?? null, [proprietaires, data.owner_user_id]);

    /*
     * Rouvrir le dialogue REPART du propriétaire imposé, ou d'un choix vide.
     * Le choix précédent reconduit ferait éditer quelqu'un qu'on n'a pas
     * demandé — et cet écran écrit sur un COMPTE.
     */
    React.useEffect(() => {
        if (!open) return;
        clearErrors();
        setRecherche('');
        setCreee(null);
        setData((previous) => ({ ...previous, owner_user_id: ownerId === null ? '' : String(ownerId) }));
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [open, ownerId]);

    /*
     * Le propriétaire choisi remplit le formulaire. Les champs de la FICHE
     * priment sur ceux du compte quand elle existe — le téléphone de la fiche
     * est celui qui part chez TEJ ; celui du compte ne sert que de première
     * valeur, faute de mieux, puisque `identiteDepuisUser()` ne le recopie pas.
     */
    React.useEffect(() => {
        if (choisi === null) return;
        setData((previous) => ({
            ...previous,
            // Un compte du manager est une personne PHYSIQUE à défaut
            // d'information : il porte un CIN, pas un matricule. Même repli que
            // `RetenueLigneService::identiteDepuisUser()`.
            type_personne: choisi.type_personne ?? 'physique',
            matricule_fiscal: choisi.matricule_fiscal ?? '',
            cin: choisi.cin ?? '',
            adresse_fiscale: choisi.adresse_fiscale ?? '',
            ville_fiscale: choisi.ville_fiscale ?? '',
            code_postal_fiscal: choisi.code_postal_fiscal ?? '',
            telephone: choisi.fiche_telephone ?? choisi.telephone ?? '',
            activite: choisi.fiche_activite ?? '',
            date_naissance: choisi.fiche_date_naissance ?? '',
        }));
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [choisi?.id]);

    const morale = data.type_personne === 'morale';

    /**
     * ═══════════════════════════════════════════════════════════════════════
     * LE RATTACHEMENT S'ANNONCE AVANT, PAS APRÈS
     * ═══════════════════════════════════════════════════════════════════════
     * Sept propriétaires du parc portent un CIN que le registre de TEJ connaît
     * déjà, sur une fiche importée sous la graphie ARABE de la DGI. Jusqu'ici
     * leur fiche fiscale était impossible à enregistrer : le formulaire refusait
     * le CIN en conseillant un rattachement qui n'existait nulle part.
     *
     * Elle est désormais rattachée automatiquement — et c'est justement pour ça
     * qu'il faut le DIRE avant le clic : sans ce bandeau, l'utilisateur verrait
     * apparaître dans sa liste une fiche portant un nom qu'il ne reconnaît pas,
     * sans jamais comprendre d'où elle sort.
     *
     * ⚠️ L'ANNONCE SUIT LA SAISIE. Le serveur a calculé cette fiche à partir de
     * l'identifiant DU COMPTE ; si l'utilisateur corrige le numéro à l'écran,
     * l'adoption ne portera plus sur elle et l'annonce doit disparaître.
     * Comparaison repliée en minuscules : un matricule s'écrit « 1631526V » ou
     * « 1631526v », et MySQL ne fait pas la différence là où JavaScript la fait.
     */
    const rattachement = React.useMemo(() => {
        const fiche = choisi?.fiche_a_rattacher ?? null;

        if (!fiche) return null;

        const saisi = (morale ? data.matricule_fiscal : data.cin).trim().toLowerCase();

        return saisi !== '' && saisi === fiche.identifiant.trim().toLowerCase() ? fiche : null;
    }, [choisi, morale, data.matricule_fiscal, data.cin]);

    const [recherche, setRecherche] = React.useState('');

    /*
     * Cherchable par TOUT ce qui distingue : nom, identifiant fiscal, courriel,
     * téléphone. Deux homonymes sans identité fiscale ne se départagent que par
     * là. `correspondARecherche` est le module pur déjà testé du sélecteur — la
     * même tolérance aux séparateurs, sans le déroulant.
     */
    const filtres = React.useMemo(
        () =>
            proprietaires.filter((owner) =>
                correspondARecherche(
                    {
                        name: owner.nom,
                        searchValue: `${owner.matricule_fiscal ?? ''} ${owner.cin ?? ''} ${owner.email ?? ''} ${owner.telephone ?? ''}`,
                    },
                    recherche,
                ),
            ),
        [proprietaires, recherche],
    );

    const submit = (event: React.FormEvent) => {
        event.preventDefault();
        if (choisi === null) return;

        const optionnel = (value: string) => (value.trim() === '' ? null : value.trim());

        form.transform((current) => ({
            type_personne: current.type_personne,
            // On n'envoie QUE l'identifiant du type choisi : laisser filer
            // l'autre écrirait un matricule sur une personne physique, que TEJ
            // ferait ensuite primer sur son CIN (`typeIdentifierPour()`).
            matricule_fiscal: morale ? optionnel(current.matricule_fiscal) : null,
            cin: morale ? null : optionnel(current.cin),
            adresse_fiscale: optionnel(current.adresse_fiscale),
            ville_fiscale: optionnel(current.ville_fiscale),
            code_postal_fiscal: optionnel(current.code_postal_fiscal),
            telephone: optionnel(current.telephone),
            activite: optionnel(current.activite),
            // Envoyée par ABSENCE quand elle est vide : un `null` effacerait une
            // date déjà enregistrée sur la fiche.
            ...(current.date_naissance !== '' ? { date_naissance: current.date_naissance } : {}),
        }));

        form.post(route('fournisseurs.proprietaire', { owner: choisi.id }), {
            preserveScroll: true,
            onSuccess: (page) => {
                const flash = page.props.flash as { fiche_id?: number; tej_envoye?: boolean } | undefined;

                // Correction (pas de `fiche_id`), fiche déjà inscrite chez TEJ,
                // ou raccourci non demandé : on ferme et on bascule directement.
                if (!onEnvoyerTej || typeof flash?.fiche_id !== 'number' || flash.tej_envoye !== false) {
                    onOpenChange(false);
                    rechargerLaListe();

                    return;
                }

                setCreee({ id: flash.fiche_id });
            },
        });
    };

    return (
        <Dialog open={open} onOpenChange={onOpenChange}>
            <DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-lg" onInteractOutside={(e) => e.preventDefault()}>
                <DialogHeader>
                    <DialogTitle>{t('fournisseurs', 'proprietaire_fiche', 'Fiche fiscale d’un propriétaire')}</DialogTitle>
                    <DialogDescription>
                        {choisi === null
                            ? t('fournisseurs', 'proprietaire_choisir_desc', 'De qui s’agit-il ? Cherchez par nom, e-mail, CIN ou matricule.')
                            : t(
                                  'fournisseurs',
                                  'proprietaire_fiche_desc',
                                  'Tout ce que TEJ exige de lui. L’identité est enregistrée sur son compte, le reste sur sa fiche.',
                              )}
                    </DialogDescription>
                </DialogHeader>

                {/* ═══════════════════════════════════════════════════════════
                    ÉTAPE 1 — CHOISIR, ET LE VOIR
                    ═══════════════════════════════════════════════════════════
                    Une LISTE à plat, pas un menu déroulant. Le déroulant de
                    `select-search` s'affiche dans le sous-arbre et non dans un
                    portail — c'est délibéré, le FocusScope de Radix lui volerait
                    le focus à chaque frappe — mais il se faisait alors ROGNER par
                    l'`overflow-y-auto` du dialogue : on ne voyait que deux lignes
                    et demie, coupées en haut et en bas.

                    Choisir un propriétaire parmi cent neuf est de toute façon une
                    tâche de liste, pas de menu : elle mérite la place. */}
                {/* ═══════════════════════════════════════════════════════════
                    DERNIÈRE ÉTAPE — le raccourci vers l'envoi.

                    Depuis le 2026-08-24, la création n'envoie plus rien toute
                    seule : cette étape est la SEULE invitation à l'envoi, et
                    elle s'affiche à chaque fiche NEUVE — sauf si elle est déjà
                    inscrite chez TEJ (une fiche restaurée qui portait déjà son
                    lien), et seulement quand /fournisseurs a passé
                    `onEnvoyerTej`. Voir `FicheCreeeStep` pour la raison de
                    cette dernière garde : proposer l'envoi d'une fiche déjà
                    inscrite inviterait à un upsert que TEJ ne refuse jamais.
                    ═══════════════════════════════════════════════════════════ */}
                {creee !== null ? (
                    <FicheCreeeStep
                        onFermer={() => {
                            onOpenChange(false);
                            rechargerLaListe();
                        }}
                        onEnvoyer={() => {
                            const id = creee.id;
                            onOpenChange(false);
                            rechargerLaListe(id);
                        }}
                    />
                ) : choisi === null ? (
                    <div className="flex flex-col gap-2">
                        <div className="relative">
                            <Search className="text-muted-foreground pointer-events-none absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2" />
                            <Input
                                autoFocus
                                value={recherche}
                                onChange={(e) => setRecherche(e.target.value)}
                                placeholder={t('fournisseurs', 'proprietaire_select', 'Chercher un propriétaire')}
                                className="h-9 pl-9"
                            />
                        </div>

                        <div className="max-h-[46vh] min-h-[16rem] overflow-y-auto rounded-md border">
                            {filtres.length === 0 ? (
                                <p className="text-muted-foreground px-3 py-8 text-center text-sm">
                                    {t('fournisseurs', 'no_result', 'Aucun résultat')}
                                </p>
                            ) : (
                                filtres.map((owner) => {
                                    const identifiant = owner.matricule_fiscal ?? owner.cin;

                                    return (
                                        <button
                                            key={owner.id}
                                            type="button"
                                            onClick={() => setData('owner_user_id', String(owner.id))}
                                            className="hover:bg-accent flex w-full flex-col gap-0.5 border-b px-3 py-2 text-left transition-colors last:border-b-0"
                                        >
                                            {/* Deux lignes par entrée, et ce n'est pas de l'ornement :
                                                « Chtourou Malek » et « Chtourou Melek » sont deux
                                                personnes, toutes deux sans identité fiscale. Le nom
                                                seul ne permettait pas de choisir — et le mauvais
                                                choix écrit un CIN sur le compte de quelqu'un d'autre. */}
                                            <span className="flex min-w-0 items-center gap-2 text-sm">
                                                <span className="min-w-0 flex-1 truncate font-medium">{owner.nom}</span>
                                                <span className={`shrink-0 text-xs ${identifiant ? 'text-muted-foreground' : 'text-amber-600'}`}>
                                                    {identifiant ?? t('fournisseurs', 'sans_identifiant', 'Sans identifiant fiscal')}
                                                </span>
                                            </span>
                                            <span className="text-muted-foreground flex min-w-0 items-center gap-2 text-xs">
                                                <span className="min-w-0 flex-1 truncate">{owner.email ?? owner.telephone ?? '—'}</span>
                                                <span className="shrink-0">
                                                    {owner.logements}{' '}
                                                    {owner.logements > 1
                                                        ? t('fournisseurs', 'logements', 'logements')
                                                        : t('fournisseurs', 'logement', 'logement')}
                                                </span>
                                                {owner.fiche_id !== null && (
                                                    <span className="shrink-0 rounded-sm border border-sky-300 bg-sky-50 px-1 text-[10px] text-sky-800">
                                                        {t('fournisseurs', 'fiche_existante', 'fiche existante')}
                                                    </span>
                                                )}
                                            </span>
                                        </button>
                                    );
                                })
                            )}
                        </div>
                        <FieldError message={errors.owner_user_id} />

                        <DialogFooter>
                            <Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
                                {t('fournisseurs', 'cancel', 'Annuler')}
                            </Button>
                        </DialogFooter>
                    </div>
                ) : (
                    <form onSubmit={submit} className="flex flex-col gap-4">
                        <div className="flex flex-col gap-1.5">
                            <Label>{t('fournisseurs', 'proprietaire', 'Propriétaire')}</Label>
                            <div className="bg-muted/40 flex items-center justify-between gap-2 rounded-md border px-3 py-2">
                                <span className="min-w-0 truncate text-sm font-medium">{choisi.nom}</span>
                                {/* Revenir en arrière n'est possible que si le
                                    propriétaire n'est pas IMPOSÉ par l'appelant :
                                    depuis une ligne de retenue, il est déjà choisi. */}
                                {ownerId === null && (
                                    <button
                                        type="button"
                                        onClick={() => setData('owner_user_id', '')}
                                        className="text-muted-foreground hover:text-foreground shrink-0 text-xs underline underline-offset-2"
                                    >
                                        {t('fournisseurs', 'changer', 'Changer')}
                                    </button>
                                )}
                            </div>
                            <FieldError message={errors.owner_user_id} />
                        </div>

                        {/* Le COURRIEL part dans `<AdresseMail>` et le dépôt le
                                réclame. Il identifie le compte, donc il n'est pas
                                saisissable ici — s'il manque, on le dit plutôt que
                                de laisser découvrir le refus. */}
                        <div className="bg-muted/30 flex flex-col gap-1 rounded-md border px-3 py-2 text-sm">
                            <span className="text-muted-foreground flex items-center gap-1.5 text-xs">
                                <Lock className="h-3 w-3 shrink-0" />
                                {t('fournisseurs', 'compte_lecture_seule', 'Compte propriétaire — se modifie dans Propriétaires.')}
                            </span>
                            <span className="flex justify-between gap-2">
                                <span className="text-muted-foreground">{t('fournisseurs', 'email', 'E-mail')}</span>
                                <span className={`truncate text-right font-medium ${choisi.email ? '' : 'text-amber-700'}`}>
                                    {choisi.email || t('fournisseurs', 'email_manquant', 'Absent — TEJ refusera le dépôt')}
                                </span>
                            </span>
                        </div>

                        <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
                            <div className="flex flex-col gap-1.5">
                                <Label>{t('fournisseurs', 'type_personne', 'Type')}</Label>
                                <Select value={data.type_personne} onValueChange={(value) => setData('type_personne', value)}>
                                    <SelectTrigger aria-invalid={!!errors.type_personne}>
                                        <SelectValue />
                                    </SelectTrigger>
                                    <SelectContent>
                                        <SelectItem value="physique">{t('fournisseurs', 'type_physique', 'Personne physique')}</SelectItem>
                                        <SelectItem value="morale">{t('fournisseurs', 'type_morale', 'Personne morale')}</SelectItem>
                                    </SelectContent>
                                </Select>
                                <FieldError message={errors.type_personne} />
                            </div>

                            {/* UN seul champ d'identifiant, celui que le type
                                    impose. Les deux côte à côte invitaient à remplir
                                    les deux, et TEJ ferait alors primer le matricule
                                    sur le CIN d'une personne physique. */}
                            <div className="flex flex-col gap-1.5">
                                <Label htmlFor="of-identifiant">
                                    {morale ? t('fournisseurs', 'matricule_fiscal', 'Matricule fiscal') : t('fournisseurs', 'cin', 'CIN')}
                                    <span className="text-destructive ml-1">*</span>
                                </Label>
                                <Input
                                    id="of-identifiant"
                                    value={morale ? data.matricule_fiscal : data.cin}
                                    onChange={(e) => setData(morale ? 'matricule_fiscal' : 'cin', e.target.value)}
                                    maxLength={morale ? 40 : 50}
                                    aria-invalid={!!(morale ? errors.matricule_fiscal : errors.cin)}
                                    className="tabular-nums"
                                />
                                <FieldError message={morale ? errors.matricule_fiscal : errors.cin} />
                            </div>
                        </div>

                        {/* Bandeau d'INFORMATION, pas d'alerte : rien n'est
                            cassé, et il n'y a rien à décider. Le geste reste
                            « Enregistrer ». */}
                        {rattachement !== null && (
                            <div className="flex flex-col gap-1 rounded-md border border-sky-300 bg-sky-50 px-3 py-2 text-sm text-sky-900">
                                <span className="flex items-center gap-1.5 font-medium">
                                    <Link2 className="h-4 w-4 shrink-0" />
                                    {t('fournisseurs', 'rattachement_titre', 'Cette personne a déjà une fiche')}
                                </span>
                                <span className="text-xs text-sky-800">
                                    {t(
                                        'fournisseurs',
                                        'rattachement_corps',
                                        ':identifiant est déjà l’identifiant de la fiche fournisseur « :fiche ». C’est donc la même personne : en enregistrant, cette fiche sera rattachée à ce propriétaire. Aucune seconde fiche ne sera créée.',
                                    )
                                        .replace(':identifiant', rattachement.identifiant)
                                        .replace(':fiche', rattachement.nom)}
                                </span>
                                {rattachement.connue_de_tej && (
                                    <span className="text-xs text-sky-800">
                                        {t(
                                            'fournisseurs',
                                            'rattachement_tej',
                                            'Cette fiche est déjà connue de la DGI : c’est elle qu’il faut garder. En créer une seconde écraserait la sienne au prochain envoi.',
                                        )}
                                    </span>
                                )}
                                {rattachement.corbeille && (
                                    <span className="text-xs text-sky-800">
                                        {t(
                                            'fournisseurs',
                                            'rattachement_corbeille',
                                            'Cette fiche avait été supprimée : le rattachement la remettra dans la liste.',
                                        )}
                                    </span>
                                )}
                            </div>
                        )}

                        <div className="grid grid-cols-1 gap-3 sm:grid-cols-4">
                            <div className="flex flex-col gap-1.5 sm:col-span-2">
                                <Label htmlFor="of-adresse">
                                    {t('fournisseurs', 'adresse', 'Adresse')}
                                    <span className="text-destructive ml-1">*</span>
                                </Label>
                                <Input
                                    id="of-adresse"
                                    value={data.adresse_fiscale}
                                    onChange={(e) => setData('adresse_fiscale', e.target.value)}
                                    maxLength={255}
                                    aria-invalid={!!errors.adresse_fiscale}
                                />
                                <FieldError message={errors.adresse_fiscale} />
                            </div>
                            <div className="flex flex-col gap-1.5">
                                <Label htmlFor="of-ville">{t('fournisseurs', 'ville', 'Ville')}</Label>
                                <Input
                                    id="of-ville"
                                    value={data.ville_fiscale}
                                    onChange={(e) => setData('ville_fiscale', e.target.value)}
                                    maxLength={100}
                                />
                                <FieldError message={errors.ville_fiscale} />
                            </div>
                            <div className="flex flex-col gap-1.5">
                                <Label htmlFor="of-cp">{t('fournisseurs', 'code_postal', 'Code postal')}</Label>
                                <Input
                                    id="of-cp"
                                    value={data.code_postal_fiscal}
                                    onChange={(e) => setData('code_postal_fiscal', e.target.value)}
                                    maxLength={20}
                                    className="tabular-nums"
                                />
                                <FieldError message={errors.code_postal_fiscal} />
                            </div>
                        </div>

                        {/* Ces trois-là vivent sur la FICHE : `users` n'a ni
                                activité ni date de naissance, et le téléphone n'est
                                pas recopié depuis le compte. Ce sont exactement ceux
                                qui manquaient — 34 des 36 fiches propriétaires sont
                                sans activité, 36 sur 36 sans date de naissance. */}
                        <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
                            <div className="flex flex-col gap-1.5">
                                <Label htmlFor="of-telephone">
                                    {t('fournisseurs', 'telephone', 'Téléphone')}
                                    <span className="text-destructive ml-1">*</span>
                                </Label>
                                <Input
                                    id="of-telephone"
                                    value={data.telephone}
                                    onChange={(e) => setData('telephone', e.target.value)}
                                    maxLength={40}
                                    aria-invalid={!!errors.telephone}
                                />
                                {/* OÙ ce numéro est enregistré. `users.phone` et
                                    `beneficiaires.telephone` sont deux colonnes
                                    distinctes que rien ne recopie l'une sur
                                    l'autre : sans cette phrase, corriger ici en
                                    croyant corriger le compte laisse les deux
                                    diverger en silence. */}
                                <p className="text-muted-foreground text-xs">
                                    {t(
                                        'fournisseurs',
                                        'telephone_fiche_hint',
                                        'Ce numéro est enregistré sur la fiche fiscale et c’est lui qui part chez la DGI. Le téléphone du compte propriétaire, lui, ne change pas.',
                                    )}
                                </p>
                                <FieldError message={errors.telephone} />
                            </div>
                            <div className="flex flex-col gap-1.5">
                                <Label>
                                    {t('fournisseurs', 'date_naissance', 'Date de naissance')}
                                    {!morale && <span className="text-destructive ml-1">*</span>}
                                </Label>
                                {/* `FrDateInput` n'expose pas `aria-invalid` : on
                                        rougit par sa classe plutôt que d'élargir son
                                        interface pour un seul appelant. */}
                                <FrDateInput
                                    value={data.date_naissance}
                                    onChange={(iso) => setData('date_naissance', iso)}
                                    max={maxDateNaissance}
                                    inputClassName={`h-9${errors.date_naissance ? ' border-destructive' : ''}`}
                                />
                                <FieldError message={errors.date_naissance} />
                            </div>
                        </div>

                        <div className="flex flex-col gap-1.5">
                            <Label htmlFor="of-activite">{t('fournisseurs', 'activite', 'Activité (facultatif)')}</Label>
                            <Input
                                id="of-activite"
                                value={data.activite}
                                onChange={(e) => setData('activite', e.target.value)}
                                maxLength={255}
                                placeholder={t('fournisseurs', 'activite_exemple', 'PHARMACIE, CONSULTANT…')}
                                aria-invalid={!!errors.activite}
                            />
                            <p className="text-muted-foreground text-xs">
                                {t(
                                    'fournisseurs',
                                    'activite_hint',
                                    'Le métier déclaré à la DGI — « PHARMACIE », « CONSULTANT ». Un propriétaire qui loue son logement n’en a généralement aucune : laissez vide.',
                                )}
                            </p>
                            <FieldError message={errors.activite} />
                        </div>

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

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