import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { ChevronDown, ChevronRight } from 'lucide-react';
import React, { useEffect, useMemo, useState } from 'react';
import {
    diffWords,
    grammarCertainty,
    spellingCertainty,
    type GrammarKind,
    type LocatedCorrection,
    type ProofreadIssue,
    type ProofreadResult,
} from './proofread';

/**
 * Relecture d'un message avant qu'il ne parte chez le voyageur.
 *
 * DEUX NIVEAUX DE CERTITUDE, et c'est tout le propos de cet écran.
 *
 * Ce qui est MÉCANIQUE est déjà appliqué à l'ouverture : la typographie, les
 * corrections de pure casse, les élisions reconstruites, les accents restaurés,
 * et la grammaire que le modèle déclare certaine. Le gérant n'a rien à cliquer
 * dessus ; il peut les replier, les relire et en annuler une.
 *
 * Ce qui est DOUTEUX attend son clic : plusieurs graphies plausibles, un mot
 * capitalisé (nom propre probable), un mot que le dictionnaire accepte et qu'on
 * soupçonne quand même, ou une correction que le modèle lui-même dit incertaine.
 *
 * Deux règles ne bougent pas. « Envoyer tel quel » renvoie le texte D'ORIGINE,
 * sans aucune correction. Et rien ne part sans que le gérant l'ait vu : le
 * message corrigé coûte UN clic, jamais zéro.
 */

interface Props {
    open: boolean;
    /** Le texte tel que le gérant l'a tapé. */
    original: string;
    result: ProofreadResult | null;
    /** Corrections grammaticales, déjà localisées sur `result.corrected`. */
    grammar: readonly LocatedCorrection[];
    /** Envoie le texte retenu et referme. */
    onSend: (text: string) => void;
    /** Referme sans envoyer et rend la main à la zone de saisie. */
    onEdit: () => void;
}

interface Replacement {
    start: number;
    end: number;
    value: string;
}

/**
 * Rejoue TOUTES les substitutions retenues depuis `corrected`, à chaque rendu.
 *
 * On ne modifie jamais le texte courant au fil des clics : les offsets des deux
 * couches portent sur `corrected`, et les décaler au fur et à mesure les ferait
 * dériver dès la deuxième correction. Le parcours part de la FIN pour que les
 * positions restantes gardent leur sens.
 */
function applyChoices(
    corrected: string,
    issues: readonly ProofreadIssue[],
    choices: Record<number, string>,
    grammar: readonly LocatedCorrection[],
    grammarChosen: ReadonlySet<number>,
): string {
    const replacements: Replacement[] = [
        ...issues
            .map((issue, index) => ({ issue, index }))
            .filter(({ index }) => typeof choices[index] === 'string')
            .map(({ issue, index }) => ({ start: issue.start, end: issue.end, value: choices[index] })),
        ...grammar.filter((_, index) => grammarChosen.has(index)).map((c) => ({ start: c.start, end: c.end, value: c.to })),
    ];

    return replacements
        .sort((a, b) => b.start - a.start)
        .reduce((text, { start, end, value }) => text.slice(0, start) + value + text.slice(end), corrected);
}

const KIND_LABELS: Record<GrammarKind, string> = {
    orthographe: 'Orthographe',
    conjugaison: 'Conjugaison',
    accord: 'Accord',
    grammaire: 'Grammaire',
};

const ProofreadDialog: React.FC<Props> = ({ open, original, result, grammar, onSend, onEdit }) => {
    const [choices, setChoices] = useState<Record<number, string>>({});
    const [grammarChosen, setGrammarChosen] = useState<ReadonlySet<number>>(() => new Set());
    const [showApplied, setShowApplied] = useState(false);

    const spellingIssues = useMemo(
        () => (result ? result.issues.filter((issue) => issue.kind === 'orthographe' && issue.suggestions.length > 0) : []),
        [result],
    );

    // Les index sont calculés UNE fois, sur les mêmes tableaux que le rendu : c'est
    // ce qui garantit qu'une puce affichée « appliquée » l'est réellement.
    const spellingSure = useMemo(
        () => spellingIssues.map((issue, index) => ({ issue, index })).filter(({ issue }) => spellingCertainty(issue) === 'certaine'),
        [spellingIssues],
    );
    const spellingToCheck = useMemo(
        () => spellingIssues.map((issue, index) => ({ issue, index })).filter(({ issue }) => spellingCertainty(issue) !== 'certaine'),
        [spellingIssues],
    );
    const grammarSure = useMemo(() => grammar.map((c, index) => ({ c, index })).filter(({ c }) => grammarCertainty(c) === 'certaine'), [grammar]);
    const grammarToCheck = useMemo(() => grammar.map((c, index) => ({ c, index })).filter(({ c }) => grammarCertainty(c) !== 'certaine'), [grammar]);

    // Une nouvelle relecture repart des corrections CERTAINES déjà cochées : c'est
    // ce qui fait tomber le nombre de clics à un. Les douteuses restent décochées.
    useEffect(() => {
        const nextChoices: Record<number, string> = {};
        for (const { issue, index } of spellingSure) nextChoices[index] = issue.suggestions[0];

        setChoices(nextChoices);
        setGrammarChosen(new Set(grammarSure.map(({ index }) => index)));
        setShowApplied(false);
    }, [result, spellingSure, grammarSure]);

    const workingText = useMemo(
        () => (result ? applyChoices(result.corrected, spellingIssues, choices, grammar, grammarChosen) : original),
        [result, spellingIssues, choices, grammar, grammarChosen, original],
    );

    const segments = useMemo(() => diffWords(original, workingText), [original, workingText]);
    const unchanged = workingText === original;
    const appliedCount = spellingSure.length + grammarSure.length;
    const toCheckCount = spellingToCheck.length + grammarToCheck.length;

    if (!result) return null;

    const toggleSpelling = (index: number, suggestion: string) =>
        setChoices((previous) => {
            const next = { ...previous };
            // Recliquer la suggestion retenue la retire : le seul moyen d'annuler
            // une correction, y compris une appliquée d'office.
            if (next[index] === suggestion) delete next[index];
            else next[index] = suggestion;
            return next;
        });

    const toggleGrammar = (index: number) =>
        setGrammarChosen((previous) => {
            const next = new Set(previous);
            if (next.has(index)) next.delete(index);
            else next.add(index);
            return next;
        });

    const renderSpelling = ({ issue, index }: { issue: ProofreadIssue; index: number }) => (
        <div key={`s-${issue.start}-${index}`} className="flex flex-wrap items-center gap-1.5">
            <span
                className={`rounded px-1.5 py-0.5 text-xs ${choices[index] ? 'text-gray-400 line-through' : 'bg-amber-100 font-medium text-amber-800'}`}
            >
                {issue.from}
            </span>
            <span className="text-xs text-gray-400">→</span>
            {issue.suggestions.map((suggestion) => (
                <button
                    key={suggestion}
                    type="button"
                    onClick={() => toggleSpelling(index, suggestion)}
                    className={`rounded-full border px-2.5 py-0.5 text-xs transition-colors ${
                        choices[index] === suggestion
                            ? 'border-[#31b08f] bg-[#31b08f] text-white'
                            : 'border-gray-200 bg-white text-gray-600 hover:border-[#31b08f] hover:text-[#31b08f]'
                    }`}
                >
                    {suggestion}
                </button>
            ))}
        </div>
    );

    const renderGrammar = ({ c, index }: { c: LocatedCorrection; index: number }) => (
        <div key={`g-${c.start}-${index}`} className="flex flex-wrap items-center gap-1.5">
            <span
                className={`rounded px-1.5 py-0.5 text-xs ${grammarChosen.has(index) ? 'text-gray-400 line-through' : 'bg-sky-100 font-medium text-sky-800'}`}
            >
                {c.from}
            </span>
            <span className="text-xs text-gray-400">→</span>
            <button
                type="button"
                onClick={() => toggleGrammar(index)}
                className={`rounded-full border px-2.5 py-0.5 text-xs transition-colors ${
                    grammarChosen.has(index)
                        ? 'border-[#31b08f] bg-[#31b08f] text-white'
                        : 'border-gray-200 bg-white text-gray-600 hover:border-[#31b08f] hover:text-[#31b08f]'
                }`}
            >
                {c.to}
            </button>
            <span className="text-[11px] text-gray-400">
                {KIND_LABELS[c.kind]} · {c.reason}
            </span>
        </div>
    );

    return (
        <Dialog open={open} onOpenChange={(next) => !next && onEdit()}>
            <DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-xl">
                <DialogHeader>
                    <DialogTitle>{toCheckCount > 0 ? 'Relecture avant envoi' : 'Message corrigé, prêt à partir'}</DialogTitle>
                </DialogHeader>

                {/* `leading-loose` et les marges des segments ne sont pas cosmétiques : le
                    diff est calculé mot à mot ET bloc d'espaces par bloc d'espaces, donc un
                    retrait et son remplacement arrivent COLLÉS. Vérifié à l'écran, sans eux
                    on lit « bonjour,votreBonjour, votre » d'une seule traite. */}
                <div className="rounded-xl border border-gray-200 bg-gray-50 p-3 text-sm leading-loose whitespace-pre-wrap">
                    {segments.map((segment, index) => {
                        if (segment.type === 'del') {
                            return (
                                <span key={index} className="mx-0.5 rounded bg-red-100 px-1 text-red-700 line-through decoration-red-400">
                                    {segment.value}
                                </span>
                            );
                        }
                        if (segment.type === 'add') {
                            return (
                                <span key={index} className="mx-0.5 rounded bg-emerald-100 px-1 font-medium text-emerald-800">
                                    {segment.value}
                                </span>
                            );
                        }
                        return <span key={index}>{segment.value}</span>;
                    })}
                </div>

                {/* Repliées par défaut : elles ne demandent RIEN au gérant. Elles restent
                    dépliables et annulables, parce qu'un correcteur qui modifie sans
                    montrer ce qu'il a fait ne se laisse pas vérifier. */}
                {appliedCount > 0 && (
                    <div className="space-y-2">
                        <button
                            type="button"
                            onClick={() => setShowApplied((v) => !v)}
                            className="flex items-center gap-1 text-xs font-semibold text-gray-600 hover:text-[#31b08f]"
                        >
                            {showApplied ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
                            {appliedCount} correction{appliedCount > 1 ? 's' : ''} appliquée{appliedCount > 1 ? 's' : ''} automatiquement
                        </button>
                        {showApplied && (
                            <div className="space-y-2 border-l-2 border-gray-100 pl-3">
                                {grammarSure.map(renderGrammar)}
                                {spellingSure.map(renderSpelling)}
                            </div>
                        )}
                    </div>
                )}

                {grammarToCheck.length > 0 && (
                    <div className="space-y-2">
                        <p className="text-xs font-semibold text-gray-600">Grammaire — à vérifier, le sens dépend de ce que vous vouliez dire</p>
                        {grammarToCheck.map(renderGrammar)}
                    </div>
                )}

                {spellingToCheck.length > 0 && (
                    <div className="space-y-2">
                        <p className="text-xs font-semibold text-gray-600">Mots à vérifier — cliquez pour remplacer, ignorez sinon</p>
                        {spellingToCheck.map(renderSpelling)}
                    </div>
                )}

                {/* Dire ce qui N'A PAS été vérifié vaut mieux que laisser croire à une
                    relecture complète. */}
                {!result.dictionaryAvailable && (
                    <p className="text-xs text-gray-500">Dictionnaire indisponible : seule la typographie a été vérifiée.</p>
                )}
                {result.dictionaryAvailable && !result.isFrench && (
                    <p className="text-xs text-gray-500">Message jugé non francophone : seule la typographie a été vérifiée.</p>
                )}

                <DialogFooter className="gap-2 sm:justify-between">
                    <button
                        type="button"
                        onClick={onEdit}
                        className="rounded-full border border-gray-200 px-4 py-2 text-sm font-medium text-gray-600 transition-colors hover:border-gray-300"
                    >
                        Modifier
                    </button>
                    <div className="flex gap-2">
                        <button
                            type="button"
                            onClick={() => onSend(original)}
                            className="rounded-full border border-gray-200 px-4 py-2 text-sm font-medium text-gray-600 transition-colors hover:border-gray-300"
                        >
                            Envoyer tel quel
                        </button>
                        <button
                            type="button"
                            onClick={() => onSend(workingText)}
                            disabled={unchanged}
                            className="rounded-full bg-[#31b08f] px-4 py-2 text-sm font-medium text-white transition-opacity hover:opacity-90 disabled:opacity-40"
                        >
                            Envoyer corrigé
                        </button>
                    </div>
                </DialogFooter>
            </DialogContent>
        </Dialog>
    );
};

export default ProofreadDialog;
