import {
    useEffect,
    useLayoutEffect,
    useMemo,
    useRef,
    useState,
    type KeyboardEvent as ReactKeyboardEvent,
    type PointerEvent as ReactPointerEvent,
    type RefObject,
} from 'react';
import {
    PINNED_FULL_WIDTH,
    applyPinnedLayout,
    computePinnedLayout,
    createPinnedWidthController,
    setTextSelectionLocked,
    type FrameScheduler,
    type PinnedLayout,
} from '../lib/pinned-layout';

/**
 * Un effet de mise en page côté navigateur, un effet ordinaire côté serveur :
 * ni l'un ni l'autre ne s'exécute pendant `build:ssr`, mais seul le premier
 * s'exécute AVANT la première peinture — la largeur mémorisée s'applique sans
 * que l'utilisateur voie d'abord la zone pleine largeur se replier.
 */
const useBrowserLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;

// Accesseurs résolus À L'APPEL, jamais au chargement du module : ce fichier est
// aussi évalué par le rendu serveur, où `window` n'existe pas.
const browserStorage = () => window.localStorage;
const browserFrames: FrameScheduler = {
    request: (callback) => window.requestAnimationFrame(callback),
    cancel: (handle) => window.cancelAnimationFrame(handle),
};
/** Racine du document, dont on verrouille la sélection de texte pendant le glissement. */
const documentRoot = () => (typeof document === 'undefined' ? null : document.documentElement);

interface UsePinnedWidthOptions {
    /** Cellule de la ligne « aucun logement » : son `colSpan` suit les colonnes figées rendues. */
    emptyCellRef: RefObject<HTMLTableCellElement | null>;
    /** Colonnes qui suivent la zone figée (les jours), ajoutées à ce `colSpan`. */
    trailingColumns: number;
}

export interface PinnedWidthControls {
    /** Disposition VALIDÉE (état React) — celle du dernier relâchement. */
    layout: PinnedLayout;
    /** Conteneur qui porte les variables CSS et les `data-hide-*` (classe `group/yt`). */
    zoneRef: RefObject<HTMLDivElement | null>;
    /** La barre elle-même (`role="separator"`). */
    separatorRef: RefObject<HTMLDivElement | null>;
    separatorHandlers: {
        onPointerDown: (event: ReactPointerEvent<HTMLDivElement>) => void;
        onPointerMove: (event: ReactPointerEvent<HTMLDivElement>) => void;
        onPointerUp: () => void;
        onPointerCancel: () => void;
        onLostPointerCapture: () => void;
        onKeyDown: (event: ReactKeyboardEvent<HTMLDivElement>) => void;
        onDoubleClick: () => void;
    };
}

/**
 * La largeur de la zone figée du tableau, et la barre qui la règle.
 *
 * ═══════════════════════════════════════════════════════════════════════════
 * POURQUOI LE GLISSEMENT NE PASSE PAS PAR REACT
 * ═══════════════════════════════════════════════════════════════════════════
 * Le tableau compte ~121 lignes × ~91 nuits, soit ~11 000 cellules. Mettre la
 * largeur dans l'état React à chaque `pointermove` referait le rendu du
 * tableau des dizaines de fois par seconde.
 *
 * Pendant le geste, on écrit donc directement dans le DOM, sur UN élément :
 * les variables CSS (`--yt-left-*`, `--yt-w-*`) et les attributs
 * `data-hide-*` du conteneur. Les cellules ne portent que des classes fixes qui
 * lisent ces variables — le navigateur recalcule la mise en page, React ne
 * voit rien. L'état React n'est validé qu'UNE fois, au relâchement.
 *
 * Et même ce rendu-là ne descend pas dans les lignes : aucune prop de `RowView`
 * ne dépend de la largeur (le masquage et le nom complet sont des sélecteurs
 * CSS), donc le `memo` les arrête toutes. Seule exception, voulue : la ligne
 * qui porte les ancres de la visite guidée, quand la colonne yield apparaît ou
 * disparaît — une ligne, au relâchement.
 *
 * Mesuré sur un banc d'essai (121 lignes × 91 nuits, appels de
 * `useTranslation` comptés) : 0 rendu pendant 120 `pointermove`, 1 rendu du
 * seul tableau au relâchement (+ 1 ligne quand la colonne yield bascule).
 * Condition : les props de `RowView` doivent rester stables — voir
 * `handleToggleYield` dans YieldTable, qui dépendait de `t` et re-rendait
 * toutes les lignes à chaque rendu du tableau. Garde-fous automatiques :
 * `createPinnedResizeGesture` (aucune validation pendant le geste) et
 * `YieldTable.row-props.test.ts` (aucune prop de ligne instable).
 *
 * Le comportement (mémorisation, double-clic, clavier, bouton principal) vit
 * dans `createPinnedWidthController`, testé sans DOM ; ce hook ne fait que
 * brancher les événements React et fournir les éléments à écrire.
 */
export function usePinnedWidth({ emptyCellRef, trailingColumns }: UsePinnedWidthOptions): PinnedWidthControls {
    // Largeur complète au premier rendu, y compris côté serveur : le
    // localStorage n'est lu qu'après montage, pour que le HTML du serveur et
    // la première passe du navigateur soient identiques (hydratation).
    const [width, setWidth] = useState(PINNED_FULL_WIDTH);
    const layout = useMemo(() => computePinnedLayout(width), [width]);

    const zoneRef = useRef<HTMLDivElement>(null);
    const separatorRef = useRef<HTMLDivElement>(null);

    const trailingRef = useRef(trailingColumns);
    useEffect(() => {
        trailingRef.current = trailingColumns;
    }, [trailingColumns]);

    // Un seul contrôleur pour toute la vie du composant : il porte la largeur
    // validée et le geste en cours. Il ne touche ni `window` ni le DOM à sa
    // création (rendu serveur) — seulement quand un événement l'appelle.
    // `emptyCellRef` est un objet ref (stable) : lu à l'appel, jamais copié.
    const [controller] = useState(() =>
        createPinnedWidthController({
            storage: browserStorage,
            frames: browserFrames,
            // Chemin RAPIDE : DOM seulement, aucun `setState`.
            apply: (nextWidth) => {
                const next = computePinnedLayout(nextWidth);
                if (zoneRef.current) {
                    applyPinnedLayout(zoneRef.current, next);
                }
                separatorRef.current?.setAttribute('aria-valuenow', String(next.width));
                if (emptyCellRef.current) {
                    emptyCellRef.current.colSpan = next.visibleCount + trailingRef.current;
                }
            },
            // Attribut hors React (il n'est jamais rendu) : sert au style « en
            // cours ». Et, le temps du geste, plus aucune sélection de texte
            // dans la page — rendue à la fin, quelle que soit la sortie.
            setResizing: (resizing) => {
                zoneRef.current?.toggleAttribute('data-resizing', resizing);
                setTextSelectionLocked(documentRoot(), resizing);
            },
            setWidth,
        }),
    );

    // Relecture de la préférence, une fois, après montage.
    useBrowserLayoutEffect(() => {
        controller.mount();
    }, [controller]);

    // Démonté EN PLEIN geste (navigation au clavier, retour du navigateur) :
    // aucun relâchement n'arrivera plus jusqu'ici. La page suivante ne doit
    // pas hériter d'un document où plus rien ne se sélectionne — le
    // contrôleur clôt le geste et retire le verrou (testé dans
    // pinned-layout.test.ts, ce nettoyage-ci dans usePinnedWidth.test.ts).
    useEffect(() => () => controller.unmount(), [controller]);

    const separatorHandlers = useMemo<PinnedWidthControls['separatorHandlers']>(
        () => ({
            onPointerDown: (event) => {
                const separator = event.currentTarget;
                const started = controller.pointerDown({
                    button: event.button,
                    clientX: event.clientX,
                    capture: () => separator.setPointerCapture(event.pointerId),
                });
                if (!started) {
                    return;
                }
                // Sans cela, le navigateur fait de cet appui le début d'une
                // sélection de texte : `select-none` sur la barre ne l'empêche
                // pas de s'étendre aux cellules de prix pendant le glissement,
                // surlignées en bleu. Mais annuler l'appui annule aussi le
                // focus qu'il aurait donné à la barre : on le rend, pour que
                // les flèches du clavier la règlent juste après, comme avant.
                event.preventDefault();
                separator.focus({ preventScroll: true });
            },
            onPointerMove: (event) => controller.pointerMove(event.clientX),
            onPointerUp: () => controller.pointerEnd(),
            onPointerCancel: () => controller.pointerEnd(),
            onLostPointerCapture: () => controller.pointerEnd(),
            onKeyDown: (event) => {
                if (controller.keyDown(event.key)) {
                    event.preventDefault();
                }
            },
            onDoubleClick: () => controller.doubleClick(),
        }),
        [controller],
    );

    return { layout, zoneRef, separatorRef, separatorHandlers };
}
