import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Spinner } from '@/components/ui/spinner';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { useTranslation } from '@/hooks/use-translation';
import { router } from '@inertiajs/react';
import { RefreshCw } from 'lucide-react';
import * as React from 'react';
import { ACTUALISATION_AUTO, badgePeriode, depotEstPerime, doitActualiserAuto, fraicheurDgi, memoireActualisation, recapDepot } from './depot';
import type { RetenueDeclaration } from './types';

/**
 * L'état DGI de la période — et le geste qui le relit.
 *
 * ═════════════════════════════════════════════════════════════════════════════
 * POURQUOI LA RELECTURE PART DE L'ÉCRAN, ET PLUS D'UNE TÂCHE PLANIFIÉE
 * ═════════════════════════════════════════════════════════════════════════════
 * Les envois partent du manager EN LIGNE, et c'est la même instance qui peut
 * relire leur état chez la DGI. La relecture était confiée à une tâche qui ne
 * tournait pas là où l'on envoie : l'utilisateur envoyait une ligne, et
 * l'écran ne découvrait jamais ni l'acceptation ni le refus. Il préfère ce qui
 * se fait seul à ce qu'il faut penser à faire ; l'écran relit donc de lui-même
 * tant qu'un envoi attend son verdict, et offre « Actualiser l'état DGI » pour
 * ne pas attendre.
 *
 * Les règles — quand, combien, à quel rythme — vivent dans `depot.ts`
 * (`doitActualiserAuto`, `ACTUALISATION_AUTO`), où elles sont testées. Ce
 * fichier ne fait que les brancher sur des minuteries et sur Inertia.
 */

/** Ce que la page reçoit du crochet, et rien de plus. */
export interface ActualisationDgi {
    /**
     * Relecture DEMANDÉE : `force: true`, visite Inertia ordinaire. Le serveur
     * lui garde un plancher d'une minute (`ANTI_REBOND_FORCE_SECONDES`) : un
     * double-clic, ou trois onglets, ne coûtent pas une passe chacun.
     */
    actualiser: () => void;
    /** Une relecture — manuelle ou automatique — est en vol. */
    enVol: boolean;
    /**
     * Ce flash vient-il d'une relecture AUTOMATIQUE ?
     *
     * Une relecture que personne n'a demandée ne toaste pas son succès : le
     * badge qui change sur la ligne EST la nouvelle, et un toast toutes les
     * cinq minutes serait un bruit qu'on apprendrait à ignorer — y compris le
     * jour où il dirait quelque chose. Ses ERREURS, elles, restent affichées.
     */
    estFlashAutomatique: (flash: unknown) => boolean;
}

/**
 * La relecture de l'état DGI : manuelle à la demande, automatique tant qu'un envoi attend.
 *
 * ═════════════════════════════════════════════════════════════════════════════
 * LA RELECTURE AUTOMATIQUE NE DOIT JAMAIS GÊNER UN GESTE DE L'UTILISATEUR
 * ═════════════════════════════════════════════════════════════════════════════
 * Une visite Inertia ordinaire INTERROMPT celle qui est en vol. Lancée toutes
 * les cinq minutes, une relecture ordinaire aurait donc pu couper
 * l'enregistrement d'un paiement en plein vol : le serveur l'aurait écrit,
 * l'écran n'en aurait jamais reçu la réponse, et le dialogue serait resté
 * ouvert sur une saisie pourtant enregistrée.
 *
 * Elle part donc en `async: true` — une visite que rien n'interrompt et qui
 * n'interrompt rien, et que la barre de chargement globale ignore : ce qu'on
 * n'a pas demandé ne doit pas s'afficher comme une attente. Et dans l'autre
 * sens, TOUT geste de l'utilisateur l'ANNULE (`router.on('before')`) : sans
 * quoi une relecture partie sur mars et revenue après un passage à avril
 * ramènerait l'écran sur mars — la réponse est une redirection vers la page
 * d'origine. L'annulation ne coûte rien : le battement suivant reprendra.
 *
 * La relecture DEMANDÉE, elle, est une visite ordinaire : c'est un geste, la
 * barre de chargement doit le montrer.
 */
export function useActualisationDgi({ peut, enCours, verifieAt }: { peut: boolean; enCours: number; verifieAt: string | null }): ActualisationDgi {
    const [enVol, setEnVol] = React.useState(false);
    const enVolRef = React.useRef(false);

    // Relectures automatiques lancées dans cet onglet — plafonnées. HORS du
    // composant : Inertia remonte l'écran à chaque enregistrement, et un
    // compteur local repartait alors de zéro (cf. `memoireActualisation()`).
    const memoire = memoireActualisation('retenue.tej.actualiser');
    const annulerAuto = React.useRef<(() => void) | null>(null);
    const flashesAuto = React.useRef(new WeakSet<object>());

    // Les minuteries lisent la DERNIÈRE valeur des props, pas celle qu'elles
    // ont capturée à leur création : une ligne envoyée après l'ouverture doit
    // être relue sans remonter l'écran.
    const derniersProps = React.useRef({ enCours, verifieAt });
    React.useEffect(() => {
        derniersProps.current = { enCours, verifieAt };
    }, [enCours, verifieAt]);

    const debut = React.useCallback(() => {
        enVolRef.current = true;
        setEnVol(true);
    }, []);

    const fin = React.useCallback(() => {
        enVolRef.current = false;
        annulerAuto.current = null;
        setEnVol(false);
    }, []);

    const actualiser = React.useCallback(() => {
        // Visite ORDINAIRE : `router.on('before')` ci-dessous annule au passage
        // une relecture automatique qui serait encore en vol.
        router.post(
            route('retenue.tej.actualiser'),
            { force: true },
            {
                preserveScroll: true,
                preserveState: true,
                onStart: debut,
                onFinish: fin,
            },
        );
    }, [debut, fin]);

    const relireSeul = React.useCallback(() => {
        memoire.tentatives += 1;
        memoire.dernierEssai = Date.now();

        router.post(
            route('retenue.tej.actualiser'),
            // `force: false` : le serveur saute lui-même la relecture si le
            // compte a été relu — ou tenté — il y a moins de cinq minutes :
            // depuis un autre onglet, un autre poste, ou par le bouton. Et il
            // la refuse quand la réserve du dépôt est atteinte.
            { force: false },
            {
                preserveScroll: true,
                preserveState: true,
                async: true,
                onCancelToken: (jeton) => {
                    annulerAuto.current = () => jeton.cancel();
                },
                onStart: debut,
                onSuccess: (page) => {
                    const flash = page.props.flash;

                    if (flash !== null && typeof flash === 'object') flashesAuto.current.add(flash);
                },
                onFinish: fin,
            },
        );
    }, [debut, fin, memoire]);

    // Tout geste de l'utilisateur annule la relecture automatique en vol.
    React.useEffect(
        () =>
            router.on('before', (event) => {
                if (!event.detail.visit.async) annulerAuto.current?.();
            }),
        [],
    );

    React.useEffect(() => {
        if (!peut) return;

        const battement = () => {
            const visible = typeof document === 'undefined' || document.visibilityState === 'visible';

            if (
                !doitActualiserAuto({
                    peut,
                    enCours: derniersProps.current.enCours,
                    verifieAt: derniersProps.current.verifieAt,
                    maintenant: new Date(),
                    tentatives: memoire.tentatives,
                    dernierEssai: memoire.dernierEssai,
                    visible,
                    enVol: enVolRef.current,
                })
            ) {
                return;
            }

            relireSeul();
        };

        // À l'ouverture d'abord, puis au battement. Le battement est LOCAL :
        // il n'appelle la DGI que si `doitActualiserAuto` le dit.
        battement();
        const minuterie = window.setInterval(battement, ACTUALISATION_AUTO.battementMs);

        // Pas d'annulation au démontage : la navigation qui démonte l'écran
        // est une visite, et `router.on('before')` l'a déjà fait.
        return () => window.clearInterval(minuterie);
    }, [peut, relireSeul, memoire]);

    const estFlashAutomatique = React.useCallback(
        (flash: unknown) => flash !== null && typeof flash === 'object' && flashesAuto.current.has(flash),
        [],
    );

    return { actualiser, enVol, estFlashAutomatique };
}

/**
 * Le badge de la période — « Déclarée » seulement quand la DGI l'a dit.
 *
 * La règle vit dans `badgePeriode()` (`depot.ts`), testée ; ce composant ne
 * fait que la peindre, avec son infobulle quand le badge ne dit pas tout.
 */
export function BadgePeriodeDgi({ declaration }: { declaration: Pick<RetenueDeclaration, 'statut' | 'depots'> }) {
    const { t } = useTranslation();
    const badge = badgePeriode(declaration, t);

    if (!badge.aide) {
        return (
            <Badge variant={badge.variant} className={badge.classe}>
                {badge.libelle}
            </Badge>
        );
    }

    return (
        <Tooltip>
            <TooltipTrigger asChild>
                <Badge variant={badge.variant} className={`cursor-default ${badge.classe}`}>
                    {badge.libelle}
                </Badge>
            </TooltipTrigger>
            <TooltipContent>{badge.aide}</TooltipContent>
        </Tooltip>
    );
}

/**
 * « 9 acceptés · 2 en attente DGI — État DGI vérifié il y a 3 min [Actualiser] ».
 *
 * ═════════════════════════════════════════════════════════════════════════════
 * LA FRAÎCHEUR PRIME SUR TOUT LE RESTE
 * ═════════════════════════════════════════════════════════════════════════════
 * Trois états, et le premier n'affiche AUCUN compteur : un « 0 refusé » est
 * une affirmation, il dit « nous avons regardé ». `recapDepot()` rend `null`
 * tant que rien n'a été vérifié. Au-delà de 24 h les compteurs passent en
 * muted : ils restent lisibles, mais cessent d'avoir l'air d'être l'état du
 * moment.
 *
 * ⚠️ CES COMPTEURS VIENNENT DE `declaration.depots`, JAMAIS DES LIGNES
 * AFFICHÉES. Elles sont filtrées ; un « 1 refusé » qui disparaîtrait en
 * cochant « Payé » serait exactement le défaut que l'avertissement en toutes
 * lettres de `RetenuePageService::props()` existe pour empêcher.
 *
 * Le bouton et l'ancre de visite n'existent que si ce compte PEUT relire la
 * DGI (compte principal, intégration active) : ailleurs, un bouton qui
 * répondrait par un refus serait pire que pas de bouton.
 */
export function EtatDgi({
    declaration,
    peutActualiser,
    actualisation,
}: {
    declaration: Pick<RetenueDeclaration, 'depots' | 'depot_verifie_at'>;
    peutActualiser: boolean;
    actualisation: ActualisationDgi;
}) {
    const { t } = useTranslation();

    // « il y a 3 min » vieillit pendant qu'on regarde l'écran : on le recalcule
    // toutes les trente secondes, et à chaque nouvelle vérification.
    const [maintenant, setMaintenant] = React.useState(() => new Date());
    React.useEffect(() => {
        const minuterie = window.setInterval(() => setMaintenant(new Date()), 30 * 1000);

        return () => window.clearInterval(minuterie);
    }, []);
    React.useEffect(() => setMaintenant(new Date()), [declaration.depot_verifie_at]);

    const recap = recapDepot(declaration, t);
    const perime = depotEstPerime(declaration.depot_verifie_at, maintenant);
    const fraicheur = fraicheurDgi(declaration.depot_verifie_at, maintenant, t);

    const etat = (
        <>
            {recap && (
                <span
                    className={perime ? 'text-muted-foreground text-xs' : 'text-xs font-medium'}
                    title={t('retenue', 'depot_recap', 'État des dépôts chez la DGI')}
                >
                    {recap}
                </span>
            )}
            <span className="text-muted-foreground text-xs">{fraicheur}</span>
        </>
    );

    if (!peutActualiser) return etat;

    return (
        <span data-tour="retenue-dgi" className="inline-flex flex-wrap items-center gap-x-2 gap-y-1">
            {etat}
            <Tooltip>
                <TooltipTrigger asChild>
                    {/* `span` intermédiaire : un bouton désactivé n'émet pas
                        d'événement de survol, donc n'ouvre aucune infobulle. */}
                    <span className={actualisation.enVol ? 'cursor-wait' : undefined}>
                        <Button
                            type="button"
                            variant="outline"
                            size="sm"
                            className="h-7 px-2 text-xs"
                            onClick={actualisation.actualiser}
                            disabled={actualisation.enVol}
                        >
                            {actualisation.enVol ? (
                                <Spinner size="sm" className="mr-1.5 text-current" />
                            ) : (
                                <RefreshCw className="mr-1.5 h-3.5 w-3.5" />
                            )}
                            {actualisation.enVol
                                ? t('retenue', 'depot_actualisation', 'Actualisation…')
                                : t('retenue', 'depot_actualiser', 'Actualiser l’état DGI')}
                        </Button>
                    </span>
                </TooltipTrigger>
                <TooltipContent>
                    {t(
                        'retenue',
                        'depot_actualiser_aide',
                        'Relit chez la DGI l’état de vos envois. Lecture seule : rien n’est déposé. L’écran le fait aussi de lui-même tant qu’un envoi attend son verdict.',
                    )}
                </TooltipContent>
            </Tooltip>
        </span>
    );
}
