import type { AppTour, AppTourStep, TourViewerContext } from './types';

/**
 * Registre des visites guidées — fonctions PURES, sans DOM ni React.
 *
 * Tout ce qui décide « quelles étapes cet utilisateur voit-il, et dans quel
 * ordre » vit ici, et nulle part ailleurs. C'est ce qui rend la règle testable
 * en environnement `node` (voir `registry.test.ts`), là où Vitest tourne dans ce
 * dépôt — sans jsdom, sans monter un composant.
 */

/**
 * Retrouve la visite déclarée pour une page Inertia.
 *
 * Le rattachement se fait sur le NOM DE COMPOSANT (`usePage().component`), pas
 * sur l'URL : l'URL porte des identifiants et des filtres qui changent à chaque
 * visite, le composant est stable.
 */
export function findTourForComponent(tours: AppTour[], component: string): AppTour | undefined {
    return tours.find((tour) => tour.components.includes(component));
}

/** L'utilisateur a-t-il le droit de voir cette étape ? */
function isStepVisible(step: AppTourStep, viewer: TourViewerContext): boolean {
    if (step.adminOnly && !viewer.isAdmin) {
        return false;
    }

    // Un administrateur voit tout : il porte rarement les permissions
    // nominativement, c'est le rôle qui ouvre les écrans.
    if (step.permission && !viewer.isAdmin && !viewer.permissions.includes(step.permission)) {
        return false;
    }

    return true;
}

/** Une étape dont l'ancre a disparu du JSX — presque toujours un renommage oublié. */
export interface MissingAnchor {
    tourId: string;
    anchor: string;
}

export interface ResolvedTour {
    steps: AppTourStep[];
    /** Ancres NON optionnelles introuvables dans le DOM — à signaler, pas à taire. */
    missing: MissingAnchor[];
}

/**
 * Calcule les étapes réellement jouables pour cet utilisateur, sur cette page,
 * à cet instant.
 *
 * Trois filtres successifs :
 *   1. les droits (permission / admin) ;
 *   2. la présence de l'ancre dans le DOM ;
 *   3. le tri par `order`.
 *
 * Une ancre absente ne fait jamais échouer la visite — elle retire l'étape. Une
 * visite qui plante parce qu'un bouton conditionnel n'est pas rendu serait
 * indébogable pour l'utilisateur, qui verrait seulement « rien ne se passe ».
 * Mais l'absence d'une ancre NON optionnelle est retournée à l'appelant, qui la
 * porte en console : silencieuse, elle laisserait une visite se vider à mesure
 * que le JSX bouge, sans que personne ne s'en aperçoive.
 */
export function resolveTourSteps(tour: AppTour, viewer: TourViewerContext): ResolvedTour {
    const missing: MissingAnchor[] = [];

    const steps = tour.steps
        .filter((step) => isStepVisible(step, viewer))
        .filter((step) => {
            if (viewer.hasAnchor(step.anchor)) {
                return true;
            }

            if (!step.optional) {
                missing.push({ tourId: tour.id, anchor: step.anchor });
            }

            return false;
        })
        .sort((a, b) => a.order - b.order);

    return { steps, missing };
}

/** Clés de traduction d'une étape, dans le namespace `tours`. */
export function stepTranslationKeys(tourId: string, step: AppTourStep): { title: string; content: string } {
    return {
        title: `${tourId}.${step.key}.title`,
        content: `${tourId}.${step.key}.content`,
    };
}

/** Sélecteur CSS de l'ancre d'une étape. */
export function anchorSelector(anchor: string): string {
    return `[data-tour="${anchor}"]`;
}

/**
 * Faut-il cliquer ce déclencheur pour l'amener à l'état voulu ?
 *
 * `state` est l'attribut `data-state` posé par Radix sur un déclencheur de
 * menu / popover : « open » ou « closed ». Un déclencheur se bascule par un
 * clic ; il ne faut donc cliquer que si l'état actuel diffère de l'état visé —
 * sinon on referme précisément ce qu'on voulait ouvrir.
 *
 * Un attribut absent vaut « fermé » : c'est l'état d'un composant qui n'a pas
 * encore été ouvert une première fois.
 */
export function needsToggle(state: string | null | undefined, shouldBeOpen: boolean): boolean {
    return (state === 'open') !== shouldBeOpen;
}

/**
 * Clé du localStorage où TourGuide note les visites terminées.
 * Voir `handlers/handleFinishTour.ts` dans la bibliothèque.
 */
export const TOUR_STORAGE_KEY = 'tg_tours_complete';

/**
 * Cette visite a-t-elle déjà été terminée par ce navigateur ?
 *
 * On relit le format de la bibliothèque (une liste de groupes séparés par des
 * virgules) plutôt que d'appeler sa méthode `isFinished`. La raison n'est pas
 * cosmétique : `isFinished` est une méthode d'INSTANCE, et instancier un client
 * juste pour poser la question forcerait le chargement du paquet sur CHAQUE
 * page — et surtout ajouterait un second dialogue au DOM, ce qui casse le
 * rendu (voir le commentaire de `use-tour.tsx`).
 */
export function isTourCompleted(raw: string | null | undefined, tourId: string): boolean {
    if (!raw) {
        return false;
    }

    return raw
        .split(',')
        .map((entry) => entry.trim())
        .includes(tourId);
}

/** Largeur maximale du dialogue de visite sur un écran assez large. */
export const TOUR_DIALOG_MAX_WIDTH = 380;

/**
 * Marge que floating-ui garde entre le dialogue et chaque bord de l'écran :
 * `shift({ padding: 15 })` dans `core/dialog.ts` de la bibliothèque.
 */
export const TOUR_DIALOG_SCREEN_MARGIN = 15;

/** Plancher, pour un écran absurdement étroit : sous 200 px, le texte ne se lirait plus. */
export const TOUR_DIALOG_MIN_WIDTH = 200;

/**
 * Largeur maximale du dialogue de visite pour un écran de `viewportWidth` px.
 *
 * 380 px fixes débordaient d'un téléphone : mesuré à 375 × 812, le dialogue
 * faisait 380 px pour un écran de 375, posé à x = −5, texte coupé au bord. On
 * retire donc de la largeur de l'écran les DEUX marges de floating-ui : plus
 * large, le dialogue ne pourrait pas les respecter des deux côtés, et
 * `shift` le collerait à un bord en laissant dépasser l'autre.
 *
 * Jamais 0, négatif ni NaN : la bibliothèque n'applique la valeur que si elle
 * est vraie (`if (dialogMaxWidth)` dans `renderDialogHtml`) et garderait
 * sinon la largeur du lancement précédent — celle d'un autre écran, peut-être.
 * Une largeur illisible (fenêtre non mesurée) rend la largeur de bureau.
 */
export function tourDialogMaxWidth(viewportWidth: number): number {
    if (!Number.isFinite(viewportWidth) || viewportWidth <= 0) {
        return TOUR_DIALOG_MAX_WIDTH;
    }

    const fitting = Math.floor(viewportWidth) - 2 * TOUR_DIALOG_SCREEN_MARGIN;

    return Math.max(TOUR_DIALOG_MIN_WIDTH, Math.min(TOUR_DIALOG_MAX_WIDTH, fitting));
}

/** Libellés des boutons du dialogue, dans la langue de l'utilisateur. */
export interface TourButtonLabels {
    next: string;
    prev: string;
    finish: string;
}

/**
 * Ce que `startTour` réinjecte dans le client unique à chaque lancement.
 * Un alias, pas une interface : il doit rester assignable au type d'options
 * du client, qui porte une signature d'index.
 */
export type TourLaunchOptions<Step> = {
    steps: Step[];
    nextLabel: string;
    prevLabel: string;
    finishLabel: string;
    dialogMaxWidth: number;
};

/**
 * Les options d'un LANCEMENT de visite, passées telles quelles à
 * `client.setOptions` dans `use-tour.tsx`.
 *
 * Tout ce qui dépend de la page, des droits, de la langue ou de l'écran — et
 * qui a donc pu changer depuis la construction du client, unique pour tout
 * l'onglet — passe par ici, jamais par le constructeur. Sortie du hook pour
 * être TESTÉE : si la largeur adaptée à l'écran disparaissait de cet objet,
 * le dialogue retrouverait la largeur précédente (380 px au premier
 * lancement) et déborderait d'un téléphone de 375 px, sans qu'aucun autre
 * test ne tombe — le hook n'est pas monté en test (pas de DOM).
 */
export function tourLaunchOptions<Step>({
    steps,
    labels,
    viewportWidth,
}: {
    steps: Step[];
    labels: TourButtonLabels;
    /** Largeur de l'écran, lue au lancement (jamais au chargement du module : rendu serveur). */
    viewportWidth: number;
}): TourLaunchOptions<Step> {
    return {
        steps,
        nextLabel: labels.next,
        prevLabel: labels.prev,
        finishLabel: labels.finish,
        dialogMaxWidth: tourDialogMaxWidth(viewportWidth),
    };
}
