import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle } from '@/components/ui/sheet';
import { Spinner } from '@/components/ui/spinner';
import { useTranslation } from '@/hooks/use-translation';
import { cn } from '@/lib/utils';
import { Link, router } from '@inertiajs/react';
import axios from 'axios';
import { ChevronDown, CircleCheck, CloudUpload, FileStack, FileText, Landmark, TriangleAlert, X } from 'lucide-react';
import { useEffect, useMemo, useRef, useState, type DragEvent, type ReactNode } from 'react';
import { toast } from 'sonner';
import { decimales, fmtDate, fmtMontant, fmtNombre, lireNombre } from '../format';
import type { CreditReleve, Devise, EtatReleve, LogementChoix, RattachementRapports, ReleveResume, SejourReleve } from '../types';
import { Point, SejourLigne } from './sejour-rapport';

interface ReleveBancaireSheetProps {
    open: boolean;
    onOpenChange: (open: boolean) => void;
    tvaDefaut: number;
}

type Regroupement = 'global' | 'proprietaire';
type Vars = Record<string, string | number>;
type Traduire = (cle: string, fr: string, vars?: Vars) => string;

/** Mêmes plafonds que `ReleveBancaireRequest` (config `factures.import.max_kb`, 40 rapports par envoi). */
const TAILLE_MAX = 10 * 1024 * 1024;
const RAPPORTS_MAX = 40;

/** Seul un versement « à facturer » (rapport + taux, pas encore facturé) se coche. */
const estCochable = (c: CreditReleve) => c.statut === 'a_facturer';
/** Un séjour compte sur la facture s'il est sans erreur et pas déjà facturé (même filtre que le serveur). */
const estFacturable = (s: SejourReleve) => s.erreur === null && s.deja_facturee === null;

/** Remplace les `:cle` connues d'un libellé traduit (et elles seules : `:n` ne mord pas sur `:numero`). */
function remplir(texte: string, vars?: Vars): string {
    if (!vars) return texte;
    return texte.replace(/:([a-z_]+)/gi, (tout, cle: string) => (cle in vars ? String(vars[cle]) : tout));
}

/** Le premier message d'une 422 Laravel, sinon le `message` de la réponse. */
function messageErreur(e: unknown): string | null {
    if (!axios.isAxiosError<{ message?: string; errors?: Record<string, string[]> }>(e)) return null;
    const data = e.response?.data;
    return (data?.errors ? Object.values(data.errors)[0]?.[0] : undefined) ?? data?.message ?? null;
}

/** « 01/04 → 30/04/2026 » ; l'année de début n'est répétée que si elle diffère. */
function periode(du: string, au: string): string {
    return `${du.slice(0, 4) === au.slice(0, 4) ? fmtDate(du).slice(0, 5) : fmtDate(du)} → ${fmtDate(au)}`;
}

/**
 * Relevé bancaire → facture de commission en dinars.
 *
 * Chaque crédit « AIRBNB PAYMENTS » du relevé est un versement ; son rapport
 * Airbnb (même montant) donne ses séjours, calculés par le serveur selon la
 * règle de paiement de chaque logement, puis convertis au taux BCT du jour du
 * versement. Le panneau montre où en est chaque versement (facturé, rapport
 * ou taux manquant, à facturer) et crée UNE facture en dinars, groupée par
 * versement. Aucun montant n'est calculé ici, hormis l'estimation de TVA.
 */
export function ReleveBancaireSheet({ open, onOpenChange, tvaDefaut }: ReleveBancaireSheetProps) {
    const { t } = useTranslation();
    const tr: Traduire = (cle, fr, vars) => remplir(t('factures', cle, fr), vars);

    const champReleve = useRef<HTMLInputElement>(null);
    const champRapports = useRef<HTMLInputElement>(null);

    const [etat, setEtat] = useState<EtatReleve | null>(null);
    const [releves, setReleves] = useState<ReleveResume[] | null>(null);
    const [listeIndisponible, setListeIndisponible] = useState(false);
    const [affectations, setAffectations] = useState<Record<string, number>>({});
    const [selection, setSelection] = useState<Set<number>>(() => new Set());
    const [deplies, setDeplies] = useState<Set<number>>(() => new Set());
    const [regroupement, setRegroupement] = useState<Regroupement>('global');
    const [rattachement, setRattachement] = useState<RattachementRapports | null>(null);
    const [saisiesTaux, setSaisiesTaux] = useState<Record<number, string>>({});
    const [erreur, setErreur] = useState<string | null>(null);
    const [survol, setSurvol] = useState<'releve' | 'rapports' | null>(null);
    // Une seule requête à la fois : chacune a son indicateur, `occupe` les réunit.
    const [lecture, setLecture] = useState(false);
    const [ouverture, setOuverture] = useState<number | null>(null);
    const [depot, setDepot] = useState(false);
    const [recalcul, setRecalcul] = useState<string | null>(null);
    const [edition, setEdition] = useState<string | null>(null);
    const [tauxEnvoi, setTauxEnvoi] = useState<number | null>(null);
    const [envoi, setEnvoi] = useState(false);

    const occupe = lecture || ouverture !== null || depot || recalcul !== null || tauxEnvoi !== null || envoi;
    const devise: Devise = etat?.releve.devise ?? 'USD';

    // Relevés déjà importés : rechargés à chaque retour sur l'écran de départ (les compteurs ont pu bouger).
    const depart = etat === null;
    useEffect(() => {
        if (!open || !depart) return;
        let actif = true;
        axios
            .get<{ releves: ReleveResume[] }>(route('factures.releves.index'))
            .then(({ data }) => {
                if (!actif) return;
                setReleves(data.releves);
                setListeIndisponible(false);
            })
            .catch(() => actif && setListeIndisponible(true));
        return () => {
            actif = false;
        };
    }, [open, depart]);

    /**
     * Nouvel état. `avant` null (relevé ouvert) : tous les versements à facturer
     * sont cochés. Sinon : les coches de l'utilisateur restent, et les versements
     * qui viennent de devenir facturables (rapport déposé, taux saisi) se cochent.
     */
    const appliquer = (data: EtatReleve, avant: EtatReleve | null) => {
        setEtat(data);
        const cochables = data.credits.filter(estCochable).map((c) => c.id);
        if (avant === null) {
            setSelection(new Set(cochables));
            return;
        }
        const deja = new Set(avant.credits.filter(estCochable).map((c) => c.id));
        setSelection((prec) => new Set(cochables.filter((id) => prec.has(id) || !deja.has(id))));
    };

    /** Nouveau relevé à l'écran : on repart de zéro. */
    const ouvrir = (data: EtatReleve) => {
        setAffectations({});
        setEdition(null);
        setDeplies(new Set());
        setRattachement(null);
        setSaisiesTaux({});
        setErreur(null);
        appliquer(data, null);
    };

    /**
     * Les réponses du dépôt de rapports et du taux sont calculées SANS les
     * logements choisis à la main : on les réapplique pour que l'écran montre
     * ce que la facture reprendra. Si ce second appel échoue, les choix sont
     * abandonnés (et on le dit) plutôt que d'afficher autre chose que ce qui
     * serait facturé.
     */
    const avecAffectations = (data: EtatReleve): Promise<EtatReleve> =>
        Object.keys(affectations).length === 0
            ? Promise.resolve(data)
            : axios
                  .post<EtatReleve>(route('factures.releves.etat', data.releve.id), { affectations })
                  .then((r) => r.data)
                  .catch(() => {
                      setAffectations({});
                      toast.warning(
                          tr(
                              'releve.affectations_perdues',
                              'Les logements choisis à la main n’ont pas pu être réappliqués : choisissez-les à nouveau.',
                          ),
                      );
                      return data;
                  });

    const lireReleve = (f: File | undefined) => {
        if (!f || occupe) return;
        if (!/\.pdf$/i.test(f.name)) {
            setErreur(tr('releve.erreur_type', 'Déposez le relevé en PDF.'));
            return;
        }
        if (f.size > TAILLE_MAX) {
            setErreur(tr('rapport.erreur_taille', 'Le fichier dépasse 10 Mo.'));
            return;
        }

        const formulaire = new FormData();
        formulaire.append('fichier', f);
        setErreur(null);
        setLecture(true);
        axios
            .post<EtatReleve>(route('factures.releves.store'), formulaire)
            .then(({ data }) => ouvrir(data))
            .catch((e: unknown) => setErreur(messageErreur(e) ?? tr('releve.erreur_lecture', 'La lecture du relevé a échoué. Réessayez.')))
            .finally(() => {
                setLecture(false);
                if (champReleve.current) champReleve.current.value = '';
            });
    };

    const rouvrir = (r: ReleveResume) => {
        if (occupe) return;
        setErreur(null);
        setOuverture(r.id);
        axios
            .post<EtatReleve>(route('factures.releves.etat', r.id), { affectations: {} })
            .then(({ data }) => ouvrir(data))
            .catch((e: unknown) => setErreur(messageErreur(e) ?? tr('releve.erreur_ouverture', 'Ce relevé ne s’ouvre pas. Réessayez.')))
            .finally(() => setOuverture(null));
    };

    const deposerRapports = (liste: FileList | null | undefined) => {
        if (!etat || occupe || !liste || liste.length === 0) return;
        const fichiers = Array.from(liste);
        const refuses = fichiers
            .filter((f) => !/\.(pdf|csv|txt)$/i.test(f.name) || f.size > TAILLE_MAX)
            .map((f) => ({ fichier: f.name, message: tr('releve.rapport_refuse', 'PDF ou CSV de 10 Mo au plus.') }));
        const valables = fichiers.filter((f) => /\.(pdf|csv|txt)$/i.test(f.name) && f.size <= TAILLE_MAX);
        const vider = () => {
            if (champRapports.current) champRapports.current.value = '';
        };

        if (valables.length > RAPPORTS_MAX) {
            toast.error(tr('releve.trop_de_rapports', ':max rapports au plus par envoi.', { max: RAPPORTS_MAX }));
            vider();
            return;
        }
        if (valables.length === 0) {
            setRattachement({ rattaches: [], sans_credit: [], erreurs: refuses });
            vider();
            return;
        }

        const formulaire = new FormData();
        valables.forEach((f) => formulaire.append('fichiers[]', f));
        const avant = etat;
        setRattachement(null);
        setDepot(true);
        axios
            .post<EtatReleve & { rattachement: RattachementRapports }>(route('factures.releves.rapports', etat.releve.id), formulaire)
            .then(async ({ data }) => {
                const { rattachement: resultat, ...suivant } = data;
                setRattachement({ ...resultat, erreurs: [...refuses, ...resultat.erreurs] });
                appliquer(await avecAffectations(suivant), avant);
            })
            .catch((e: unknown) => toast.error(messageErreur(e) ?? tr('releve.erreur_rapports', 'L’envoi des rapports a échoué. Réessayez.')))
            .finally(() => {
                setDepot(false);
                vider();
            });
    };

    const enregistrerTaux = (c: CreditReleve) => {
        if (!etat || occupe) return;
        const taux = lireNombre(saisiesTaux[c.id] ?? '');
        if (taux <= 0) {
            toast.error(tr('releve.taux_invalide', 'Saisissez un taux positif, par exemple 2,9329.'));
            return;
        }
        const avant = etat;
        setTauxEnvoi(c.id);
        axios
            .post<EtatReleve>(route('factures.releves.taux', etat.releve.id), { date: c.date_operation, taux })
            .then(async ({ data }) => {
                setSaisiesTaux((prec) => {
                    const suivantes = { ...prec };
                    delete suivantes[c.id];
                    return suivantes;
                });
                appliquer(await avecAffectations(data), avant);
            })
            .catch((e: unknown) => toast.error(messageErreur(e) ?? tr('releve.erreur_taux', 'Le taux n’a pas été enregistré. Réessayez.')))
            .finally(() => setTauxEnvoi(null));
    };

    const affecter = (cle: string, ligne: string, logementId: number) => {
        if (!etat || occupe) return;
        const suivantes = { ...affectations, [cle]: logementId };
        const avant = etat;
        setRecalcul(ligne);
        axios
            .post<EtatReleve>(route('factures.releves.etat', etat.releve.id), { affectations: suivantes })
            .then(({ data }) => {
                setAffectations(suivantes);
                setEdition(null);
                appliquer(data, avant);
            })
            .catch((e: unknown) => toast.error(messageErreur(e) ?? tr('rapport.erreur_recalcul', 'Le recalcul a échoué. Réessayez.')))
            .finally(() => setRecalcul(null));
    };

    const recommencer = () => {
        setEtat(null);
        setAffectations({});
        setSelection(new Set());
        setDeplies(new Set());
        setRattachement(null);
        setSaisiesTaux({});
        setEdition(null);
        setErreur(null);
    };

    const credits = useMemo(() => etat?.credits ?? [], [etat]);
    const cochables = useMemo(() => credits.filter(estCochable), [credits]);
    const choisis = useMemo(() => credits.filter((c) => estCochable(c) && selection.has(c.id)), [credits, selection]);

    const totaux = useMemo(() => {
        const htTnd = choisis.reduce((acc, c) => acc + c.total_ht_tnd, 0);
        const ht = choisis.reduce((acc, c) => acc + c.total_ht, 0);
        const sejours = choisis.flatMap((c) => c.sejours.filter(estFacturable));
        return {
            htTnd,
            ht,
            tva: (htTnd * tvaDefaut) / 100,
            lignes: sejours.length,
            proprietaires: new Set(sejours.map((s) => s.logement?.proprietaire_id ?? 0)).size,
        };
    }, [choisis, tvaDefaut]);

    const nbBrouillons = totaux.lignes === 0 ? 0 : regroupement === 'global' ? 1 : totaux.proprietaires;
    const toutCoche = cochables.length > 0 && cochables.every((c) => selection.has(c.id));
    const sansRapport = credits.filter((c) => c.statut !== 'facture' && c.rapport === null).length;

    const basculer = (id: number) =>
        setSelection((prec) => {
            const suivante = new Set(prec);
            if (suivante.has(id)) suivante.delete(id);
            else suivante.add(id);
            return suivante;
        });

    const deplier = (id: number) =>
        setDeplies((prec) => {
            const suivante = new Set(prec);
            if (suivante.has(id)) suivante.delete(id);
            else suivante.add(id);
            return suivante;
        });

    const toutBasculer = () => setSelection(toutCoche ? new Set() : new Set(cochables.map((c) => c.id)));

    // Jamais fermé pendant un envoi de fichier ou la création de la facture.
    const fermer = (ouvert: boolean) => {
        if (lecture || depot || envoi) return;
        onOpenChange(ouvert);
    };

    const creer = () => {
        if (!etat || choisis.length === 0 || totaux.lignes === 0 || occupe) return;
        router.post(
            route('factures.releves.facture', etat.releve.id),
            { credit_ids: choisis.map((c) => c.id), affectations, regroupement },
            {
                preserveScroll: true,
                preserveState: 'errors',
                onStart: () => setEnvoi(true),
                onFinish: () => setEnvoi(false),
                onError: (errors) =>
                    toast.error(errors.credit_ids ?? Object.values(errors)[0] ?? tr('releve.erreur_creation', 'Création impossible.')),
            },
        );
    };

    const glisser = (zone: 'releve' | 'rapports') => ({
        onDragOver: (e: DragEvent<HTMLDivElement>) => {
            e.preventDefault();
            setSurvol(zone);
        },
        onDragLeave: () => setSurvol(null),
        onDrop: (e: DragEvent<HTMLDivElement>) => {
            e.preventDefault();
            setSurvol(null);
            if (zone === 'releve') lireReleve(e.dataTransfer.files?.[0]);
            else deposerRapports(e.dataTransfer.files);
        },
    });

    const libelleCreer =
        nbBrouillons > 1 ? tr('releve.creer_n', 'Créer :n brouillons', { n: nbBrouillons }) : tr('releve.creer', 'Créer la facture en dinars');

    return (
        <Sheet open={open} onOpenChange={fermer}>
            <SheetContent side="right" className="w-full gap-0 sm:max-w-3xl">
                <SheetHeader className="border-b pr-10">
                    <SheetTitle>{tr('releve.titre', 'Relevé bancaire')}</SheetTitle>
                    <SheetDescription>
                        {tr(
                            'releve.description',
                            'Chaque versement Airbnb du relevé devient un groupe de lignes de commission, converti en dinars au taux BCT de son jour. Déposez le relevé, puis les rapports Airbnb de ses versements.',
                        )}
                    </SheetDescription>
                </SheetHeader>

                <div className="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto p-4">
                    {etat === null ? (
                        <>
                            {lecture ? (
                                <div
                                    className="flex flex-col items-center gap-3 rounded-xl border border-dashed px-4 py-10 text-center"
                                    role="status"
                                >
                                    <Spinner size="lg" />
                                    <div className="text-sm font-medium">{tr('releve.lecture', 'Lecture du relevé et recherche des taux BCT…')}</div>
                                </div>
                            ) : (
                                <div
                                    {...glisser('releve')}
                                    className={cn(
                                        'flex flex-col items-center gap-3 rounded-xl border-2 border-dashed px-4 py-10 text-center transition-colors',
                                        survol === 'releve' ? 'border-secondary bg-secondary/5' : 'border-border bg-muted/30',
                                    )}
                                >
                                    <CloudUpload
                                        className={cn('size-8', survol === 'releve' ? 'text-secondary' : 'text-muted-foreground')}
                                        aria-hidden
                                    />
                                    <div className="text-sm">
                                        <span className="font-medium">{tr('releve.deposer', 'Glissez le relevé bancaire ici')}</span>{' '}
                                        <span className="text-muted-foreground">{tr('import.ou', 'ou')}</span>
                                    </div>
                                    <Button type="button" variant="outline" size="sm" onClick={() => champReleve.current?.click()} disabled={occupe}>
                                        {tr('import.parcourir', 'Parcourir')}
                                    </Button>
                                    <div className="text-muted-foreground max-w-md text-xs">
                                        {tr(
                                            'releve.contraintes',
                                            'PDF du relevé de compte en devise, 10 Mo au plus. Déposer à nouveau un relevé déjà importé le met à jour sans rien perdre.',
                                        )}
                                    </div>
                                    <input
                                        ref={champReleve}
                                        type="file"
                                        accept=".pdf,application/pdf"
                                        className="sr-only"
                                        tabIndex={-1}
                                        onChange={(e) => lireReleve(e.target.files?.[0])}
                                    />
                                </div>
                            )}
                            {erreur && <Alerte ton="rose">{erreur}</Alerte>}

                            {/* Relevés déjà importés */}
                            <section className="grid gap-2">
                                <h3 className="text-sm font-medium">{tr('releve.deja_importes', 'Relevés déjà importés')}</h3>
                                {releves === null ? (
                                    listeIndisponible ? (
                                        <p className="text-muted-foreground text-xs">
                                            {tr('releve.liste_indisponible', 'La liste des relevés est indisponible pour le moment.')}
                                        </p>
                                    ) : (
                                        <div className="flex justify-center py-4">
                                            <Spinner size="sm" />
                                        </div>
                                    )
                                ) : releves.length === 0 ? (
                                    <p className="text-muted-foreground text-xs">
                                        {tr('releve.aucun_releve', 'Aucun relevé importé pour l’instant.')}
                                    </p>
                                ) : (
                                    <ul className="shrink-0 divide-y overflow-hidden rounded-lg border">
                                        {releves.map((r) => {
                                            const complet = r.credits > 0 && r.factures === r.credits;
                                            return (
                                                <li key={r.id}>
                                                    <button
                                                        type="button"
                                                        onClick={() => rouvrir(r)}
                                                        disabled={occupe}
                                                        className="hover:bg-muted/30 flex w-full items-center gap-3 px-3 py-2.5 text-left transition-colors disabled:cursor-default disabled:opacity-60"
                                                    >
                                                        <Landmark className="text-muted-foreground size-4 shrink-0" aria-hidden />
                                                        <span className="grid min-w-0 flex-1 gap-0.5">
                                                            <span className="text-sm font-medium tabular-nums">{periode(r.du, r.au)}</span>
                                                            <span className="text-muted-foreground truncate text-xs">
                                                                {r.banque} · {r.compte} · {r.devise}
                                                            </span>
                                                        </span>
                                                        {ouverture === r.id ? (
                                                            <Spinner size="sm" />
                                                        ) : (
                                                            <Pastille ton={complet ? 'emerald' : 'slate'}>
                                                                {tr('releve.factures_sur', ':n / :total facturés', {
                                                                    n: r.factures,
                                                                    total: r.credits,
                                                                })}
                                                            </Pastille>
                                                        )}
                                                    </button>
                                                </li>
                                            );
                                        })}
                                    </ul>
                                )}
                            </section>
                        </>
                    ) : (
                        <>
                            {/* Fichier du relevé */}
                            <div className="flex items-center gap-3 rounded-lg border px-3 py-2">
                                <FileText className="text-primary size-5 shrink-0" aria-hidden />
                                <div className="min-w-0 flex-1">
                                    <div className="truncate text-sm font-medium" title={etat.releve.fichier ?? undefined}>
                                        {etat.releve.fichier ?? `${etat.releve.banque} ${etat.releve.compte}`}
                                    </div>
                                    <div className="text-muted-foreground truncate text-xs tabular-nums">
                                        {etat.releve.banque} · {etat.releve.compte} · {etat.releve.devise} · {periode(etat.releve.du, etat.releve.au)}
                                    </div>
                                </div>
                                <Button type="button" variant="outline" size="sm" onClick={recommencer} disabled={occupe}>
                                    {tr('releve.autre', 'Autre relevé')}
                                </Button>
                            </div>

                            <ControleReleve etat={etat} tr={tr} />

                            {/* Le relevé en une ligne */}
                            <div className="bg-muted/40 flex flex-wrap items-center gap-x-3 gap-y-1 rounded-lg border px-3 py-2 text-sm tabular-nums">
                                <span>
                                    <span className="font-semibold">{credits.length}</span> {tr('releve.versements', 'versement(s) Airbnb')}
                                </span>
                                <Point />
                                <span className="font-medium">{fmtMontant(etat.totaux.versements, devise)}</span>
                                <Point />
                                <span>
                                    ≈ <span className="text-primary font-semibold">{fmtMontant(etat.totaux.versements_tnd, 'TND')}</span>{' '}
                                    {tr('releve.aux_taux_du_jour', 'aux taux du jour')}
                                </span>
                                {etat.totaux.taux_manquants > 0 && (
                                    <>
                                        <Point />
                                        <span
                                            className="font-medium text-amber-800"
                                            title={tr('releve.taux_manquants_aide', 'Ces versements ne sont pas comptés dans le total en dinars.')}
                                        >
                                            {tr('releve.taux_manquants', ':n taux manquant(s)', { n: etat.totaux.taux_manquants })}
                                        </span>
                                    </>
                                )}
                            </div>

                            {/* Rapports Airbnb des versements */}
                            <div
                                {...glisser('rapports')}
                                className={cn(
                                    'flex flex-col items-center gap-2 rounded-xl border-2 border-dashed px-4 py-5 text-center transition-colors',
                                    survol === 'rapports' ? 'border-secondary bg-secondary/5' : 'border-border bg-muted/30',
                                )}
                            >
                                {depot ? (
                                    <div className="flex flex-col items-center gap-2 py-2" role="status">
                                        <Spinner />
                                        <div className="text-sm font-medium">
                                            {tr('releve.lecture_rapports', 'Lecture des rapports et rattachement aux versements…')}
                                        </div>
                                    </div>
                                ) : (
                                    <>
                                        <FileStack
                                            className={cn('size-6', survol === 'rapports' ? 'text-secondary' : 'text-muted-foreground')}
                                            aria-hidden
                                        />
                                        <div className="text-sm">
                                            <span className="font-medium">
                                                {tr('releve.deposer_rapports', 'Déposez les rapports Airbnb de ces versements (plusieurs à la fois)')}
                                            </span>{' '}
                                            <span className="text-muted-foreground">{tr('import.ou', 'ou')}</span>
                                        </div>
                                        <Button
                                            type="button"
                                            variant="outline"
                                            size="sm"
                                            onClick={() => champRapports.current?.click()}
                                            disabled={occupe}
                                        >
                                            {tr('import.parcourir', 'Parcourir')}
                                        </Button>
                                        <div className="text-muted-foreground max-w-md text-xs">
                                            {tr(
                                                'releve.rapports_contraintes',
                                                'PDF ou CSV, 10 Mo au plus chacun. Chaque rapport rejoint le versement du même montant.',
                                            )}
                                        </div>
                                        {sansRapport > 0 ? (
                                            <div className="text-xs font-medium text-amber-800">
                                                {tr('releve.sans_rapport', ':n versement(s) attendent leur rapport', { n: sansRapport })}
                                            </div>
                                        ) : (
                                            <div className="text-xs font-medium text-emerald-800">
                                                {tr('releve.tous_rapports', 'Chaque versement a son rapport.')}
                                            </div>
                                        )}
                                    </>
                                )}
                                <input
                                    ref={champRapports}
                                    type="file"
                                    multiple
                                    accept=".pdf,.csv,.txt,application/pdf,text/csv"
                                    className="sr-only"
                                    tabIndex={-1}
                                    onChange={(e) => deposerRapports(e.target.files)}
                                />
                            </div>

                            {rattachement && (
                                <ResultatRattachement resultat={rattachement} devise={devise} tr={tr} onFermer={() => setRattachement(null)} />
                            )}

                            {/* Versements */}
                            <div className="shrink-0 overflow-hidden rounded-lg border">
                                <div className="bg-muted/40 flex items-center gap-3 border-b px-3 py-2">
                                    <Checkbox
                                        checked={toutCoche ? true : choisis.length === 0 ? false : 'indeterminate'}
                                        onCheckedChange={toutBasculer}
                                        disabled={occupe || cochables.length === 0}
                                        aria-label={tr('commission.tout_cocher', 'Tout cocher')}
                                    />
                                    <span className="text-muted-foreground flex-1 text-xs font-medium">
                                        {tr('releve.col_versement', 'Versement')}
                                    </span>
                                    <span className="text-muted-foreground text-xs font-medium">{tr('releve.col_montant', 'Montant')}</span>
                                </div>
                                <ul className="divide-y">
                                    {credits.map((c) => (
                                        <CreditLigne
                                            key={c.id}
                                            credit={c}
                                            devise={devise}
                                            logements={etat.logements}
                                            tr={tr}
                                            cochee={selection.has(c.id)}
                                            deplie={deplies.has(c.id)}
                                            occupe={occupe}
                                            saisieTaux={saisiesTaux[c.id] ?? ''}
                                            enregistrementTaux={tauxEnvoi === c.id}
                                            edition={edition}
                                            recalcul={recalcul}
                                            onBasculer={() => basculer(c.id)}
                                            onDeplier={() => deplier(c.id)}
                                            onSaisieTaux={(v) => setSaisiesTaux((prec) => ({ ...prec, [c.id]: v }))}
                                            onEnregistrerTaux={() => enregistrerTaux(c)}
                                            onEdition={setEdition}
                                            onAffecter={affecter}
                                        />
                                    ))}
                                </ul>
                            </div>
                            <p className="text-muted-foreground text-xs">
                                {tr(
                                    'releve.exclus',
                                    'Seuls les versements « À facturer » se cochent : il leur faut leur rapport Airbnb et leur taux du jour. Un versement facturé redevient facturable si sa facture est supprimée ou annulée.',
                                )}
                            </p>
                        </>
                    )}
                </div>

                {etat !== null && (
                    <SheetFooter className="mt-0 gap-3 border-t pr-24">
                        <RadioGroup
                            value={regroupement}
                            onValueChange={(v) => setRegroupement(v === 'proprietaire' ? 'proprietaire' : 'global')}
                            className="grid gap-2 sm:grid-cols-2"
                            disabled={occupe}
                        >
                            {(
                                [
                                    {
                                        valeur: 'global',
                                        titre: tr('commission.global', 'Une facture « Propriétaires »'),
                                        aide: tr('commission.global_aide', "Comme aujourd'hui : un seul client, une ligne par réservation"),
                                    },
                                    {
                                        valeur: 'proprietaire',
                                        titre: tr('commission.par_proprietaire', 'Une facture par propriétaire'),
                                        aide: tr('commission.par_proprietaire_aide', 'Chaque propriétaire reçoit la sienne, à son nom'),
                                    },
                                ] as const
                            ).map((o) => (
                                <Label
                                    key={o.valeur}
                                    htmlFor={`releve-regroupement-${o.valeur}`}
                                    className={cn(
                                        'flex cursor-pointer items-start gap-3 rounded-lg border p-3 font-normal transition-colors',
                                        regroupement === o.valeur ? 'border-secondary bg-secondary/5' : 'hover:bg-muted/40',
                                    )}
                                >
                                    <RadioGroupItem value={o.valeur} id={`releve-regroupement-${o.valeur}`} className="mt-0.5" />
                                    <span className="grid gap-0.5">
                                        <span className="text-sm font-medium">{o.titre}</span>
                                        <span className="text-muted-foreground text-xs">{o.aide}</span>
                                    </span>
                                </Label>
                            ))}
                        </RadioGroup>

                        <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
                            <div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-sm tabular-nums">
                                <span>
                                    <span className="font-semibold">{choisis.length}</span> {tr('releve.coches', 'versement(s) coché(s)')}
                                </span>
                                <Point />
                                <span>
                                    {tr('commission.commission_ht', 'Commission HT')}{' '}
                                    <span className="text-primary font-semibold">{fmtMontant(totaux.htTnd, 'TND')}</span>
                                    {totaux.ht > 0 && <span className="text-muted-foreground"> ({fmtMontant(totaux.ht, devise)})</span>}
                                </span>
                                <Point />
                                <span title={tr('commission.tva_estimee', 'Estimation : le montant exact est calculé sur la facture.')}>
                                    {tr('commission.tva', 'TVA :taux %', { taux: tvaDefaut })} ≈ {fmtMontant(totaux.tva, 'TND')}
                                </span>
                            </div>
                            <Button
                                type="button"
                                className="bg-secondary hover:bg-secondary/90 shrink-0"
                                onClick={creer}
                                disabled={choisis.length === 0 || totaux.lignes === 0 || occupe}
                            >
                                {envoi && <Spinner size="sm" className="text-white" />}
                                {libelleCreer}
                            </Button>
                        </div>
                    </SheetFooter>
                )}
            </SheetContent>
        </Sheet>
    );
}

const TONS = {
    emerald: 'bg-emerald-50 text-emerald-800 ring-emerald-200',
    amber: 'bg-amber-50 text-amber-800 ring-amber-200',
    slate: 'bg-slate-100 text-slate-700 ring-slate-200',
} as const;

function Pastille({ ton, children, title }: { ton: keyof typeof TONS; children: ReactNode; title?: string }) {
    return (
        <span className={cn('rounded-full px-2 py-0.5 text-[11px] font-medium whitespace-nowrap ring-1 ring-inset', TONS[ton])} title={title}>
            {children}
        </span>
    );
}

function Alerte({ ton, children }: { ton: 'rose' | 'amber'; children: ReactNode }) {
    return (
        <div
            className={cn(
                'flex items-start gap-2 rounded-lg border px-3 py-2 text-sm',
                ton === 'rose' ? 'border-rose-200 bg-rose-50 text-rose-800' : 'border-amber-200 bg-amber-50 text-amber-900',
            )}
            role="alert"
        >
            <TriangleAlert className="mt-0.5 size-4 shrink-0" aria-hidden />
            <div className="min-w-0">{children}</div>
        </div>
    );
}

/** Contrôle du relevé : solde de départ + crédits − débits = solde final, vérifié à la lecture. */
function ControleReleve({ etat, tr }: { etat: EtatReleve; tr: Traduire }) {
    const { releve } = etat;
    const d = decimales(releve.devise);
    const n = (v: number) => v.toLocaleString('fr-FR', { minimumFractionDigits: d, maximumFractionDigits: d });

    return releve.controle_ok ? (
        <div className="flex items-start gap-2 rounded-lg border border-emerald-200 bg-emerald-50 px-3 py-2 text-sm text-emerald-900" role="status">
            <CircleCheck className="mt-0.5 size-4 shrink-0" aria-hidden />
            <span className="tabular-nums">
                {tr('releve.controle_ok', 'Solde vérifié : départ :depart + crédits − débits = :final.', {
                    depart: n(releve.solde_depart),
                    final: fmtMontant(releve.solde_final, releve.devise),
                })}
            </span>
        </div>
    ) : (
        <div className="flex items-start gap-2 rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-900" role="status">
            <TriangleAlert className="mt-0.5 size-4 shrink-0" aria-hidden />
            <span>{tr('releve.controle_ecart', 'Les totaux du relevé ne se recoupent pas : vérifiez-le avant de facturer.')}</span>
        </div>
    );
}

/** Ce que le dernier dépôt de rapports a donné : rattachés, illisibles, sans versement correspondant. */
function ResultatRattachement({
    resultat,
    devise,
    tr,
    onFermer,
}: {
    resultat: RattachementRapports;
    devise: Devise;
    tr: Traduire;
    onFermer: () => void;
}) {
    const n = resultat.rattaches.length;
    const rien = n === 0 && resultat.erreurs.length === 0 && resultat.sans_credit.length === 0;

    return (
        <div className="grid gap-2 rounded-lg border px-3 py-2 text-sm" role="status">
            <div className="flex items-start gap-2">
                {n > 0 ? (
                    <CircleCheck className="mt-0.5 size-4 shrink-0 text-emerald-700" aria-hidden />
                ) : (
                    <TriangleAlert className="mt-0.5 size-4 shrink-0 text-amber-700" aria-hidden />
                )}
                <span className={cn('flex-1 font-medium', n > 0 ? 'text-emerald-900' : 'text-amber-900')}>
                    {n === 0
                        ? tr('releve.rattaches_aucun', 'Aucun rapport rattaché.')
                        : n === 1
                          ? tr('releve.rattaches_un', '1 rapport rattaché à son versement.')
                          : tr('releve.rattaches_n', ':n rapports rattachés à leur versement.', { n })}
                    {rien && ` ${tr('releve.rattaches_rien', 'Les fichiers ne contenaient aucun versement.')}`}
                </span>
                <button
                    type="button"
                    onClick={onFermer}
                    className="text-muted-foreground hover:bg-muted hover:text-foreground inline-flex size-5 shrink-0 items-center justify-center rounded-full"
                    aria-label={tr('releve.fermer_resultat', 'Masquer ce résultat')}
                >
                    <X className="size-3.5" />
                </button>
            </div>
            {resultat.erreurs.length > 0 && (
                <ul className="grid gap-1 rounded-md border border-rose-200 bg-rose-50 px-3 py-2 text-xs text-rose-800">
                    {resultat.erreurs.map((e, i) => (
                        <li key={`${e.fichier}-${i}`} className="break-words">
                            <span className="font-medium">« {e.fichier} »</span> : {e.message}
                        </li>
                    ))}
                </ul>
            )}
            {resultat.sans_credit.length > 0 && (
                <ul className="grid gap-1 rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-900">
                    {resultat.sans_credit.map((s, i) => (
                        <li key={`${s.fichier}-${i}`} className="break-words">
                            <span className="font-medium">« {s.fichier} »</span> :{' '}
                            {s.montant === null
                                ? tr('releve.sans_payout', 'pas de ligne « Payout » : impossible de trouver son versement')
                                : tr('releve.sans_credit', 'rapport de :montant : aucun versement de ce montant sur ce relevé', {
                                      montant: fmtMontant(s.montant, devise),
                                  })}
                        </li>
                    ))}
                </ul>
            )}
        </div>
    );
}

interface CreditLigneProps {
    credit: CreditReleve;
    devise: Devise;
    logements: LogementChoix[];
    tr: Traduire;
    cochee: boolean;
    deplie: boolean;
    occupe: boolean;
    saisieTaux: string;
    enregistrementTaux: boolean;
    /** Ligne de séjour dont le choix du logement est ouvert (`<credit>:<cle>`). */
    edition: string | null;
    /** Ligne de séjour en cours de recalcul (`<credit>:<cle>`). */
    recalcul: string | null;
    onBasculer: () => void;
    onDeplier: () => void;
    onSaisieTaux: (valeur: string) => void;
    onEnregistrerTaux: () => void;
    onEdition: (ligne: string | null) => void;
    onAffecter: (cle: string, ligne: string, logementId: number) => void;
}

/** Un versement : date, référence, montant, taux du jour, statut — et, déplié, ses séjours. */
function CreditLigne({
    credit: c,
    devise,
    logements,
    tr,
    cochee,
    deplie,
    occupe,
    saisieTaux,
    enregistrementTaux,
    edition,
    recalcul,
    onBasculer,
    onDeplier,
    onSaisieTaux,
    onEnregistrerTaux,
    onEdition,
    onAffecter,
}: CreditLigneProps) {
    const cochable = estCochable(c);
    const facturables = c.sejours.filter(estFacturable).length;
    const dejaFactures = c.sejours.filter((s) => s.deja_facturee !== null).length;
    const aVerifier = c.sejours.length - facturables - dejaFactures;
    const tauxSaisi = lireNombre(saisieTaux);
    const ecartRapport = c.rapport !== null && Math.abs(c.rapport.payout - c.rapport.somme) >= 0.01;

    return (
        <li
            onClick={() => cochable && !occupe && onBasculer()}
            className={cn(
                'grid grid-cols-[auto_minmax(0,1fr)_auto] gap-x-3 px-3 py-3',
                cochable ? 'hover:bg-muted/30 cursor-pointer' : c.statut === 'facture' && 'bg-muted/20',
            )}
        >
            <Checkbox
                checked={cochable && cochee}
                disabled={!cochable || occupe}
                onClick={(e) => e.stopPropagation()}
                onCheckedChange={onBasculer}
                aria-label={tr('releve.cocher', 'Versement du :date', { date: fmtDate(c.date_operation) })}
                className="mt-0.5"
            />
            <div className="grid min-w-0 gap-1">
                <div className="flex flex-wrap items-center gap-x-2 gap-y-0.5">
                    <span className="text-sm font-medium tabular-nums">{fmtDate(c.date_operation)}</span>
                    {c.date_valeur && c.date_valeur !== c.date_operation && (
                        <span className="text-muted-foreground text-xs tabular-nums">
                            {tr('releve.valeur', 'valeur :date', { date: fmtDate(c.date_valeur).slice(0, 5) })}
                        </span>
                    )}
                    <StatutCredit credit={c} tr={tr} />
                </div>
                <div className="text-muted-foreground flex min-w-0 flex-wrap items-center gap-x-2 gap-y-0.5 text-xs">
                    {c.reference_airbnb ? (
                        <span className="max-w-full truncate font-mono" title={c.reference_airbnb}>
                            {c.reference_airbnb}
                        </span>
                    ) : (
                        <span className="tabular-nums">{tr('releve.ref_banque', 'réf. banque :ref', { ref: c.reference_banque })}</span>
                    )}
                    <TauxPuce credit={c} devise={devise} tr={tr} />
                    {c.frais !== null && (
                        <span className="tabular-nums">{tr('releve.frais', 'frais :montant', { montant: fmtMontant(c.frais, devise) })}</span>
                    )}
                </div>

                {/* Taux introuvable : saisi à la main, avec l'aperçu en dinars pour repérer une faute de frappe. */}
                {c.taux === null && c.statut !== 'facture' && (
                    <div className="flex flex-wrap items-center gap-2 pt-1" onClick={(e) => e.stopPropagation()}>
                        <label htmlFor={`releve-taux-${c.id}`} className="text-xs text-amber-900">
                            {tr('releve.taux_saisir', 'Cours :devise du :date introuvable :', {
                                devise: devise,
                                date: fmtDate(c.date_operation),
                            })}
                        </label>
                        <Input
                            id={`releve-taux-${c.id}`}
                            inputMode="decimal"
                            autoComplete="off"
                            placeholder="2,9329"
                            value={saisieTaux}
                            disabled={occupe}
                            onChange={(e) => onSaisieTaux(e.target.value.replace(/[^\d,.]/g, ''))}
                            onKeyDown={(e) => {
                                if (e.key === 'Enter') {
                                    e.preventDefault();
                                    onEnregistrerTaux();
                                }
                            }}
                            className="h-8 w-24 bg-white text-right text-sm tabular-nums"
                        />
                        <Button
                            type="button"
                            variant="outline"
                            size="sm"
                            className="h-8"
                            onClick={onEnregistrerTaux}
                            disabled={occupe || tauxSaisi <= 0}
                        >
                            {enregistrementTaux && <Spinner size="sm" />}
                            {tr('releve.enregistrer_taux', 'Enregistrer')}
                        </Button>
                        {tauxSaisi > 0 && (
                            <span className="text-muted-foreground text-xs tabular-nums">
                                = {fmtMontant(Math.round(c.montant * tauxSaisi * 1000) / 1000, 'TND')}
                            </span>
                        )}
                    </div>
                )}

                {c.rapport !== null && (
                    <button
                        type="button"
                        onClick={(e) => {
                            e.stopPropagation();
                            onDeplier();
                        }}
                        aria-expanded={deplie}
                        className="hover:text-foreground text-muted-foreground flex w-fit max-w-full items-start gap-1 text-left text-xs tabular-nums"
                    >
                        <ChevronDown className={cn('mt-0.5 size-3.5 shrink-0 transition-transform', deplie && 'rotate-180')} aria-hidden />
                        <span>
                            {tr('releve.resume_sejours', ':n séjour(s) · commission HT :ht', {
                                n: c.sejours.length,
                                ht: fmtMontant(c.total_ht, devise),
                            })}
                            {c.taux !== null && (
                                <>
                                    {' = '}
                                    <span className="text-foreground font-medium">{fmtMontant(c.total_ht_tnd, 'TND')}</span>
                                </>
                            )}
                            {aVerifier > 0 && (
                                <span className="font-medium text-amber-800">
                                    {' · '}
                                    {tr('releve.a_verifier', ':n à vérifier', { n: aVerifier })}
                                </span>
                            )}
                            {dejaFactures > 0 && <> · {tr('releve.deja_factures', ':n déjà facturé(s)', { n: dejaFactures })}</>}
                            {ecartRapport && (
                                <span className="font-medium text-amber-800">
                                    {' · '}
                                    {tr('releve.rapport_incomplet', 'rapport incomplet')}
                                </span>
                            )}
                        </span>
                    </button>
                )}
            </div>
            <div className="text-right tabular-nums">
                <div className="text-sm font-semibold whitespace-nowrap">{fmtMontant(c.montant, devise)}</div>
                <div className="text-muted-foreground text-[11px] whitespace-nowrap">
                    {c.montant_tnd !== null ? fmtMontant(c.montant_tnd, 'TND') : '— TND'}
                </div>
            </div>

            {deplie && c.rapport !== null && (
                <div className="col-span-3 mt-3 grid cursor-default gap-2" onClick={(e) => e.stopPropagation()}>
                    <div className="text-muted-foreground flex min-w-0 items-center gap-1.5 text-xs">
                        <FileText className="size-3.5 shrink-0" aria-hidden />
                        <span className="truncate" title={c.rapport.fichier ?? undefined}>
                            {c.rapport.fichier ?? tr('releve.rapport', 'Rapport Airbnb')}
                        </span>
                    </div>
                    {ecartRapport && (
                        <Alerte ton="amber">
                            <span className="text-xs">
                                {tr('rapport.controle_ecart', 'Les lignes font :somme pour un versement de :payout : une ligne n’a pas été lue.', {
                                    somme: fmtMontant(c.rapport.somme, devise),
                                    payout: fmtMontant(c.rapport.payout, devise),
                                })}
                            </span>
                        </Alerte>
                    )}
                    <ul className="divide-y overflow-hidden rounded-md border bg-white">
                        {c.sejours.map((s) => {
                            const ligne = `${c.id}:${s.cle}`;
                            return (
                                <SejourLigne
                                    key={ligne}
                                    sejour={s}
                                    devise={devise}
                                    logements={logements}
                                    tr={tr}
                                    actif={estFacturable(s)}
                                    occupe={occupe || c.statut === 'facture'}
                                    enEdition={edition === ligne}
                                    enRecalcul={recalcul === ligne}
                                    onEdition={(ouverte) => onEdition(ouverte ? ligne : null)}
                                    onAffecter={(logementId) => onAffecter(s.cle, ligne, logementId)}
                                    enDinars
                                />
                            );
                        })}
                    </ul>
                </div>
            )}
        </li>
    );
}

/** Statut du versement — c'est le suivi : où il a été facturé, ou ce qui lui manque. */
function StatutCredit({ credit: c, tr }: { credit: CreditReleve; tr: Traduire }) {
    if (c.statut === 'facture' && c.facture) {
        return (
            <Link
                href={route('factures.edit', c.facture.id)}
                onClick={(e) => e.stopPropagation()}
                className={cn('rounded-full px-2 py-0.5 text-[11px] font-medium whitespace-nowrap ring-1 ring-inset hover:underline', TONS.emerald)}
            >
                {c.facture.numero
                    ? tr('releve.statut_facture', 'Facturé · :numero', { numero: c.facture.numero })
                    : tr('releve.statut_brouillon', 'Facturé · dans un brouillon')}
            </Link>
        );
    }
    if (c.statut === 'rapport_manquant') return <Pastille ton="amber">{tr('releve.statut_rapport_manquant', 'Rapport manquant')}</Pastille>;
    if (c.statut === 'taux_manquant') return <Pastille ton="amber">{tr('releve.statut_taux_manquant', 'Taux manquant')}</Pastille>;
    return <Pastille ton="slate">{tr('releve.statut_a_facturer', 'À facturer')}</Pastille>;
}

/** « BCT 2,9329 », « du 03/04 » quand le cours est celui d'un autre jour (week-end, férié), « Taux saisi » à la main. */
function TauxPuce({ credit: c, devise, tr }: { credit: CreditReleve; devise: Devise; tr: Traduire }) {
    if (c.taux === null) return null;
    const valeur = fmtNombre(c.taux.valeur, 4);
    const autreJour = c.taux.date_cotation !== c.date_operation;

    return (
        <Pastille
            ton="slate"
            title={tr('releve.taux_aide', '1 :devise = :taux TND, cours du :date', {
                devise,
                taux: valeur,
                date: fmtDate(c.taux.date_cotation),
            })}
        >
            {c.taux.source === 'manuel'
                ? tr('releve.taux_manuel', 'Taux saisi :taux', { taux: valeur })
                : tr('releve.taux_bct', 'BCT :taux', { taux: valeur })}
            {autreJour && ` ${tr('releve.taux_du', 'du :date', { date: fmtDate(c.taux.date_cotation).slice(0, 5) })}`}
        </Pastille>
    );
}
