/* SPDX-FileCopyrightText: 2026 Com'Together
   SPDX-License-Identifier: AGPL-3.0-only
   Mapper BORNÉ jetons de l'appli -> structure jsRealB (syntaxe avancée, opt-in).
   PUR & TESTABLE : reçoit l'API jsRealB en paramètre (J), renvoie une phrase ou null.
   null = motif non couvert => l'appli fait un repli sur le moteur linéaire (Sentence.build).
   Verrouillé par tests/run-jsr-map.cjs (corpus-or, même bundle jsRealB que l'appli). */
(function (root, factory) {
  if (typeof module !== "undefined" && module.exports) module.exports = factory();
  else root.JSRMap = factory();
})(typeof self !== "undefined" ? self : this, function () {
  "use strict";
  const T = { present: "p", imparfait: "i", futursimple: "f", passe: "pc", conditionnel: "c" };
  const SUBJ = { "je": [1, "s"], "j'": [1, "s"], "tu": [2, "s"], "il": [3, "s"], "elle": [3, "s", "f"], "on": [3, "s"], "nous": [1, "p"], "vous": [2, "p"], "ils": [3, "p"], "elles": [3, "p", "f"] };
  const COD = new Set(["le", "la", "les", "l'"]), COI = new Set(["lui", "leur"]);
  // Négation simple « ne … pas » -> .typ({neg}). Les autres (plus/jamais/rien/…) => repli honnête.
  const NEG_SIMPLE = new Set(["ne pas", "pas", "ne", "n'"]);
  const NEG_ANY = new Set(["ne pas", "pas", "ne", "n'", "plus", "ne plus", "jamais", "ne jamais", "rien", "ne rien", "point", "guère"]);
  const low = s => (s || "").toLowerCase().trim();

  // Motif : [pronom sujet] [verbe] [clitiques objets] (+ négation « ne…pas »). Renvoie null si non mappable.
  function mapTokens(J, tokens, ctx) {
    if (!J || !tokens || !tokens.length) return null;
    ctx = ctx || {};
    const s = low(tokens[0].label);
    if (!SUBJ[s]) return null;
    const vtok = tokens.find(t => t.verb);
    if (!vtok) return null;
    const objs = tokens.filter(t => { const l = low(t.label); return COD.has(l) || COI.has(l); });
    if (!objs.length) return null;                          // sans clitique : on laisse le moteur linéaire
    if (tokens.filter(t => t.verb).length > 1) return null; // verbe gouverné : hors motif -> repli
    // Négation : présente avec des clitiques, il FAUT la porter (sinon on sortirait l'affirmatif).
    const negToks = tokens.filter(t => t.color === "negation" || NEG_ANY.has(low(t.label)));
    const applyNeg = negToks.length > 0;
    if (applyNeg && !negToks.every(t => NEG_SIMPLE.has(low(t.label)))) return null; // plus/jamais/rien… => repli
    try {
      const [pe, n, g] = SUBJ[s];
      let subj = J.Pro(s).pe(pe).n(n); if (g) subj = subj.g(g);
      const vp = J.VP(J.V(vtok.verb || vtok.label).t(T[ctx.tense] || "p"));
      objs.forEach(o => {
        const l = low(o.label), isCod = COD.has(l);
        // Base : COD -> "le" (la/les = même lemme accordé) ; COI -> "lui", sauf "leur" (lemme pluriel propre).
        const base = isCod ? "le" : (l === "leur" ? "leur" : "lui");
        let p = J.Pro(base).c(isCod ? "acc" : "dat");
        // Genre/nombre déduits DU LIBELLÉ (la=fém, les=pluriel), puis complétés par d'éventuels drapeaux.
        let g = (l === "la") ? "f" : null, plur = (l === "les");
        if (o.fem) g = "f";
        if (o.plural && isCod) plur = true;   // "leur" porte déjà le pluriel -> ne pas ajouter .n("p") (=> "leurs")
        if (g) p = p.g(g);
        if (plur) p = p.n("p");
        vp.add(p);
      });
      let sent = J.S(subj, vp); if (applyNeg) sent = sent.typ({ neg: true });
      let str = "" + sent;
      // Normaliser au style de l'appli (minuscule initiale, sans point final).
      str = str.trim().replace(/\s+/g, " ").replace(/\s*\.\s*$/, "").replace(/^(\p{L})/u, c => c.toLowerCase());
      // Registre oral : le « ne » est très souvent omis (« je le lui donne pas »), comme le moteur habituel.
      if (applyNeg && ctx.registre === "oral") str = str.replace(/\bne\s+/g, "").replace(/\bn'/g, "").replace(/\s+/g, " ").trim();
      return str || null;
    } catch (e) { return null; }
  }
  return { mapTokens };
});
