/*
|--------------------------------------------------------------------------
| Correcteur de la messagerie — ce qui s'applique seul, ce qui attend le gérant
|--------------------------------------------------------------------------
|
| Une seule règle gouverne ce fichier : CE QUI EST CERTAIN S'APPLIQUE TOUT SEUL, CE QUI EST
| DOUTEUX ATTEND LE GÉRANT. Sans elle, relire un message coûte un clic par faute — quinze
| accents oubliés, quinze cases à cocher — et personne ne relit plus rien.
|
| Sont tenues pour CERTAINES trois familles de corrections, et rien d'autre :
|
|   1. la casse seule (« test » → « Test ») : aucune lettre ne bouge, il n'y a rien à relire ;
|   2. l'accent restauré (« sejour » → « séjour ») : même suite de lettres, l'une accentuée ;
|   3. l'apostrophe d'élision remise (« dorthographe » → « d'orthographe ») : mêmes lettres,
|      une apostrophe en plus.
|
| POURQUOI celles-là et pas les autres : mesurées sur un corpus de 55 messages réels, l'élision
| reconstruite et l'accent restauré tombent juste à 95,6 %, alors que les suggestions génériques
| de nspell tombent à 0 % dès qu'il s'agit d'un nom propre — « Sabri » s'y fait proposer
| « S'abri », qui est pourtant une élision reconstruite en bonne et due forme. D'où le garde-fou
| de `spellingCertainty` : un mot qui commence par une capitale n'est JAMAIS réécrit d'office,
| sauf quand la correction ne touche que la casse.
|
| Le défaut est prudent partout : dans le doute, 'a-verifier'. Se tromper de ce côté-là coûte un
| clic ; se tromper de l'autre modifie le message parti chez le voyageur sans que personne ne
| l'ait lu.
|
| Module volontairement AUTONOME : aucune importation, aucun type du dossier, aucun dictionnaire.
| Il ne réclame que les quelques champs dont il a besoin, si bien qu'il se teste sans charger
| l'asset du dictionnaire et sert aussi bien la couche orthographique (`ProofreadIssue`) que la
| couche grammaticale (`LocatedCorrection`). Le discriminant du point 2 est celui de
| `findAccentRepair` (`dictionary.ts`), reproduit ici plutôt qu'importé pour garder cette
| indépendance.
*/

/** Verdict porté sur une correction : appliquée d'office, ou soumise au gérant. */
export type Certitude = 'certaine' | 'a-verifier';

/**
 * `true` quand les deux graphies ne diffèrent QUE par la casse (« test » / « Test »).
 *
 * Deux chaînes identiques rendent `false` : il n'y a alors aucun changement à qualifier.
 */
export function isCasingOnly(from: string, to: string): boolean {
    return from !== to && from.toLocaleLowerCase('fr') === to.toLocaleLowerCase('fr');
}

/**
 * `true` quand `to` est `from` à l'accentuation près (« sejour » → « séjour »), et non une
 * autre graphie.
 *
 * POURQUOI comparer les squelettes de lettres plutôt que se fier au rang de la suggestion :
 * nspell propose d'abord celle qui SUPPRIME une lettre — « arrive » avant « arrivée », « loue »
 * avant « louée ». Appliquée, elle fabrique une faute. Le squelette écarte ces amputations
 * (« arrivee » et « arrive » n'ont pas les mêmes lettres) sans rien perdre des vraies remises
 * d'accent. C'est le discriminant déjà mesuré dans `dictionary.ts`.
 *
 * Une pure différence de casse satisfait aussi ce prédicat, « test » et « Test » ayant le même
 * squelette. Sans conséquence : `spellingCertainty` teste la casse d'abord, et les deux mènent
 * de toute façon à 'certaine'.
 */
export function isAccentRestoration(from: string, to: string): boolean {
    return from !== to && letterSkeleton(from) === letterSkeleton(to);
}

/**
 * `true` quand `to` est `from` avec UNE apostrophe insérée, aux accents et à la casse près
 * (« dorthographe » → « d'orthographe », « larrivee » → « l'arrivée »).
 *
 * L'accent gagné au passage est VOULU : qui colle son élision oublie le plus souvent l'accent
 * du même coup, et les deux réparations n'en font qu'une. En revanche « jenvois » → « envois »
 * est refusé : là une lettre disparaît, c'est une amputation déguisée et non une apostrophe
 * remise.
 *
 * Les apostrophes courbes sont ramenées à l'apostrophe droite avant le comptage, comme partout
 * ailleurs dans le correcteur : le clavier iOS en pose une courbe, le clavier français une
 * droite, et la correction est pourtant la même.
 */
export function isElisionRebuild(from: string, to: string): boolean {
    const origine = normalizeApostrophes(from);
    const cible = normalizeApostrophes(to);

    if (countApostrophes(cible) !== countApostrophes(origine) + 1) {
        return false;
    }

    return letterSkeleton(cible.replace(/'/g, '')) === letterSkeleton(origine);
}

/**
 * `true` si le mot commence par une capitale.
 *
 * POURQUOI les deux tests : dans une écriture SANS casse — l'arabe, l'hébreu, les chiffres —
 * `toLocaleUpperCase` est l'identité, donc la seule comparaison aux majuscules déclarerait
 * capitale n'importe quel « مرحبا ». Ce piège a déjà coûté un défaut à ce correcteur : exiger
 * aussi que la minuscule DIFFÈRE ne laisse passer que les écritures qui connaissent la casse.
 */
export function startsWithCapital(word: string): boolean {
    // Déstructurer une chaîne l'itère par POINT DE CODE : un caractère hors BMP reste entier.
    const [first = ''] = word;

    return first !== '' && first.toLocaleUpperCase('fr') === first && first.toLocaleLowerCase('fr') !== first;
}

/**
 * Verdict pour un signalement de la couche ORTHOGRAPHE (`ProofreadIssue`), sur sa première
 * suggestion — c'est elle, et elle seule, qu'une application automatique poserait dans le texte.
 */
export function spellingCertainty(issue: { rule: string; from: string; suggestions: readonly string[] }): Certitude {
    // (a) Le dictionnaire ACCEPTE ces mots-là — « cava » est le passé simple de caver. On ne
    // réécrit pas d'office un mot valide : le signaler suffit, trancher appartient au gérant.
    if (issue.rule === 'mot-suspect') {
        return 'a-verifier';
    }

    // (b) Rien à proposer : il n'y a même pas de correction à appliquer.
    if (issue.suggestions.length === 0) {
        return 'a-verifier';
    }

    const suggestion = issue.suggestions[0];

    if (suggestion === '') {
        return 'a-verifier';
    }

    // (c) AVANT le garde-fou des capitales, sinon « test » → « Test » réclamerait un clic alors
    // que c'est exactement le genre de correction qui doit se faire toute seule.
    if (isCasingOnly(issue.from, suggestion)) {
        return 'certaine';
    }

    // (d) Nom propre probable. Mesuré : sur les noms propres, les suggestions de nspell tombent
    // à 0 % de justesse — « Sabri » se fait proposer « S'abri ». Le prénom du voyageur ne se
    // corrige pas sans relecture.
    if (startsWithCapital(issue.from)) {
        return 'a-verifier';
    }

    // (e) Les deux familles mesurées à 95,6 % de justesse sur 55 messages réels.
    if (isElisionRebuild(issue.from, suggestion) || isAccentRestoration(issue.from, suggestion)) {
        return 'certaine';
    }

    // (f) Suggestion générique : le dictionnaire a proposé un AUTRE mot, pas le même mieux écrit.
    return 'a-verifier';
}

/**
 * Verdict pour une correction de la couche GRAMMAIRE (`LocatedCorrection`), dont le serveur
 * renseigne `confidence` quand le modèle sait dire s'il est sûr de lui.
 *
 * POURQUOI `confidence` absent vaut 'a-verifier' : un champ optionnel manque aussi bien parce
 * que le modèle a hésité que parce que la réponse vient d'une version antérieure du contrat.
 * Le défaut prudent est celui qui coûte un clic, pas celui qui modifie le texte sans relecture.
 */
export function grammarCertainty(correction: { from: string; to: string; confidence?: string }): Certitude {
    // (a) Comme pour l'orthographe, la casse passe devant : « Nous » → « nous » ne change aucune
    // lettre, y compris sur un mot capitalisé.
    if (isCasingOnly(correction.from, correction.to)) {
        return 'certaine';
    }

    // (b) Un « sure » sur un nom propre reste un pari du modèle : il ne connaît ni le voyageur,
    // ni le nom du logement.
    if (startsWithCapital(correction.from)) {
        return 'a-verifier';
    }

    return correction.confidence === 'sure' ? 'certaine' : 'a-verifier';
}

/**
 * Sépare une liste en deux, dans l'ordre d'origine : ce qui s'applique seul, ce qui attend.
 *
 * Générique et sans dépendance aux types du dossier, pour servir aussi bien aux signalements
 * d'orthographe qu'aux corrections grammaticales. L'entrée n'est jamais mutée : l'appelant
 * garde sa liste complète pour l'aperçu avant/après.
 */
export function partition<T>(items: readonly T[], certainty: (item: T) => Certitude): { certaines: T[]; aVerifier: T[] } {
    const certaines: T[] = [];
    const aVerifier: T[] = [];

    for (const item of items) {
        if (certainty(item) === 'certaine') {
            certaines.push(item);
        } else {
            aVerifier.push(item);
        }
    }

    return { certaines, aVerifier };
}

/**
 * Suite de lettres nue : accents retirés, casse ignorée. « hôtel », « Hotel » et « hotel »
 * rendent tous « hotel » ; « arrive » et « arrivée » ne se confondent pas.
 */
function letterSkeleton(word: string): string {
    return word
        .normalize('NFD')
        .replace(/\p{Diacritic}/gu, '')
        .toLocaleLowerCase('fr');
}

/** Ramène toutes les apostrophes à la droite, la seule que l'on compte ensuite. */
function normalizeApostrophes(word: string): string {
    return word.replace(/[’‘ʼ]/gu, "'");
}

function countApostrophes(word: string): number {
    return word.split("'").length - 1;
}
