/* Helpers de dictée partagés par les drills libres, le vocabulaire et le runner. */

function normalizeDictationText(value, options) {
  const opts = options || {};
  let out = String(value || "")
    .normalize("NFC")
    .replace(/\s+/g, " ")
    .trim();
  if (opts.ignorePunct !== false) out = out.replace(/[.,!?;:…"'“”‘’()［\][\]{}<>。？！、]/g, "");
  if (opts.ignoreSpaces) out = out.replace(/\s+/g, "");
  return out.trim();
}

function normalizeDictationErrorTags(tags) {
  // Taxonomie dictée (PROBLEMATIQUES_SEANCES §6.1) : on distingue vraiment la faute
  // d'écoute (son/batchim) de la faute d'orthographe (forme), de la particule, de
  // l'ordre et de la terminaison — au lieu de tout replier sur « son ».
  const allowed = { son: true, forme: true, batchim: true, espace: true, particule: true, terminaison: true, ordre: true, aide: true };
  const aliases = { sound: "son", space: "espace", particle: "particule", ending: "terminaison", form: "forme", spelling: "forme", hangeul: "forme", order: "ordre", sens: "son", meaning: "son" };
  const list = Array.isArray(tags) ? tags : [];
  const out = [];
  list.forEach((tag) => {
    const raw = window.LangValidate && window.LangValidate.canonicalTag
      ? window.LangValidate.canonicalTag(tag)
      : String(tag || "").trim().toLowerCase();
    const key = aliases[raw] || raw;
    if (allowed[key] && out.indexOf(key) < 0) out.push(key);
  });
  return out.length ? out : ["son"];
}

const DICTATION_PUNCT_RE = /[.,!?;:…"'“”‘’()［\][\]{}<>。？！、]+/g;
const DICTATION_PARTICLES = ["은", "는", "이", "가", "을", "를", "에", "에서", "에게", "한테", "로", "으로", "와", "과", "도", "만", "의"];
const DICTATION_ENDINGS = ["이에요", "예요", "어요", "아요", "해요", "세요", "습니다", "ㅂ니다", "습니까", "ㅂ니까"];

function isHangulSyllableChar(ch) {
  const code = String(ch || "").charCodeAt(0);
  return code >= 0xAC00 && code <= 0xD7A3;
}

function cleanDictationText(text) {
  return String(text || "").normalize("NFC").replace(DICTATION_PUNCT_RE, "").trim();
}

function hasBatchimText(text) {
  const chars = Array.from(String(text || "")).filter(isHangulSyllableChar);
  if (!chars.length) return false;
  const code = chars[chars.length - 1].charCodeAt(0) - 0xAC00;
  return code >= 0 && code % 28 > 0;
}

function endsWithAny(text, list) {
  const s = String(text || "");
  return (list || []).some(tail => tail && s.endsWith(tail));
}

function dictationTagsForText(text, kind) {
  const tags = ["son"];
  if (hasBatchimText(text)) tags.push("batchim");
  if (endsWithAny(text, DICTATION_PARTICLES)) tags.push("particule");
  if (endsWithAny(text, DICTATION_ENDINGS)) tags.push("terminaison");
  if (kind === "line" || /\s/.test(String(text || ""))) tags.push("espace");
  return normalizeDictationErrorTags(tags);
}

function compareDictationAnswer(expected, answer, options) {
  const opts = Object.assign({ ignoreSpaces: false, ignorePunct: true }, options || {});
  if (window.LangValidate && window.LangValidate.compare) {
    const result = window.LangValidate.compare(expected, answer, opts);
    return Object.assign({}, result, { tags: result.correct ? [] : normalizeDictationErrorTags(result.tags) });
  }
  const e = normalizeDictationText(expected, opts);
  const g = normalizeDictationText(answer, opts);
  const correct = !!g && g === e;
  const tags = correct ? [] : (g && g.replace(/\s/g, "") === e.replace(/\s/g, "") ? ["espace"] : ["son"]);
  return { correct, expected: e, given: g, tags };
}

function splitDictationSegments(text) {
  return String(text || "")
    .normalize("NFC")
    .replace(/[.,!?;:…"'“”‘’()［\][\]{}<>。？！、]+/g, "")
    .trim()
    .split(/\s+/)
    .filter(Boolean);
}

function splitDictationSyllables(text) {
  return Array.from(cleanDictationText(text).replace(/\s+/g, "")).filter(isHangulSyllableChar);
}

function makeDictationUnit(idPrefix, kind, text, index, audio) {
  const unitText = String(text || "").trim();
  return {
    id: `${idPrefix || "dict"}-${kind}-${index + 1}`,
    order: index + 1,
    kind,
    text: unitText,
    audio: audio || { ref: unitText },
    audioTags: dictationTagsForText(unitText, kind),
  };
}

function buildDictationLevels(text, idPrefix, lineAudio) {
  const clean = cleanDictationText(text);
  const syllables = splitDictationSyllables(clean).map((s, i) => makeDictationUnit(idPrefix, "syllable", s, i, { ref: s }));
  const blocks = splitDictationSegments(clean).map((s, i) => makeDictationUnit(idPrefix, "block", s, i, { ref: s }));
  const line = clean ? [makeDictationUnit(idPrefix, "line", clean, 0, lineAudio || { ref: clean })] : [];
  return [
    { id: "syllables", order: 1, kind: "syllable", label: "Syllabes", units: syllables, audioTags: normalizeDictationErrorTags(["son", "batchim"]) },
    { id: "blocks", order: 2, kind: "block", label: "Blocs", units: blocks, audioTags: normalizeDictationErrorTags(["son", "batchim", "particule", "terminaison"]) },
    { id: "line", order: 3, kind: "line", label: "Phrase entière", units: line, audioTags: normalizeDictationErrorTags(["son", "batchim", "espace", "particule", "terminaison"]) },
  ].filter(level => level.units.length);
}

function normalizeDictationLevel(level, index) {
  const units = (Array.isArray(level && level.units) ? level.units : Array.isArray(level && level.items) ? level.items : [])
    .map((unit, i) => {
      const text = unit && (unit.text || unit.kr || unit.value || "");
      return Object.assign({}, unit, {
        id: (unit && unit.id) || `dict-${(level && level.id) || "level"}-${i + 1}`,
        order: (unit && unit.order) || i + 1,
        text,
        audio: (unit && unit.audio) || { ref: text },
        audioTags: normalizeDictationErrorTags((unit && (unit.audioTags || unit.tags)) || dictationTagsForText(text, level && level.kind)),
      });
    })
    .filter(unit => unit.text);
  return {
    id: (level && level.id) || `level_${index + 1}`,
    order: (level && level.order) || index + 1,
    kind: (level && level.kind) || "block",
    label: (level && level.label) || `Niveau ${index + 1}`,
    units,
    audioTags: normalizeDictationErrorTags((level && level.audioTags) || units.reduce((acc, unit) => acc.concat(unit.audioTags || []), [])),
  };
}

function dictationLevelsForExercise(exercise) {
  const prompt = (exercise && exercise.prompt) || {};
  const expected = (exercise && exercise.expected) || {};
  const raw = Array.isArray(prompt.levels) && prompt.levels.length
    ? prompt.levels
    : buildDictationLevels(expected.kr || expected.model || prompt.lineKr || "", (exercise && exercise.id) || "dict", prompt.audio);
  return raw.map(normalizeDictationLevel).filter(level => level.units.length);
}

function dictationLevelsForLine(line) {
  return buildDictationLevels(line && line.kr, (line && line.id) || "line", line && line.audio);
}

function dictationUnitsForLevel(exercise, levelId) {
  const levels = dictationLevelsForExercise(exercise);
  const wanted = levelId || "blocks";
  const level = levels.find(item => item.id === wanted) || levels.find(item => item.id === "blocks") || levels[0];
  return level ? level.units : [];
}

function dictationSegmentsForExercise(exercise) {
  const prompt = (exercise && exercise.prompt) || {};
  const expected = (exercise && exercise.expected) || {};
  const raw = Array.isArray(prompt.segments) && prompt.segments.length
    ? prompt.segments
    : splitDictationSegments(expected.kr || expected.model || "");
  return raw
    .map((segment, index) => {
      if (segment && typeof segment === "object") {
        const text = segment.text || segment.kr || "";
        return {
          id: segment.id || `seg_${index + 1}`,
          text,
          order: segment.order || index + 1,
          audio: segment.audio || { ref: text },
        };
      }
      const text = String(segment || "");
      return { id: `seg_${index + 1}`, text, order: index + 1, audio: { ref: text } };
    })
    .filter(segment => segment.text);
}

function compareDictationUnits(units, answers) {
  const list = Array.isArray(units) ? units : [];
  const given = Array.isArray(answers) ? answers : [];
  const results = list.map((unit, index) => compareDictationAnswer(unit.text, given[index] || "", {
    ignoreSpaces: true,
    ignorePunct: true,
  }));
  const tags = [];
  results.forEach((result) => {
    (result.tags || []).forEach((tag) => {
      if (tags.indexOf(tag) < 0) tags.push(tag);
    });
  });
  return {
    correct: results.length > 0 && results.every(result => result.correct),
    results,
    tags,
  };
}

function compareDictationSegments(segments, answers) {
  return compareDictationUnits(segments, answers);
}

function revealDictationAnswer(expected, answer) {
  const result = compareDictationAnswer(expected, answer);
  return Object.assign({}, result, { revealed: true, tags: result.given ? result.tags : ["aide"] });
}

function dictationFeedbackLabel(result) {
  if (!result) return "";
  const tags = result.tags || [];
  if (result.correct) return "Exact";
  if (result.revealed && !result.given) return "Modèle";
  // Ordre teachable d'abord, puis du plus grammatical au plus phonétique : on nomme
  // l'erreur principale plutôt qu'un « À reprendre » indistinct.
  if (tags.indexOf("particule") >= 0) return "Particule à reprendre";
  if (tags.indexOf("terminaison") >= 0) return "Terminaison à reprendre";
  if (tags.indexOf("ordre") >= 0) return "Ordre des mots à revoir";
  if (tags.indexOf("espace") >= 0) return "Espaces à revoir";
  if (tags.indexOf("batchim") >= 0) return "Batchim à reprendre";
  if (tags.indexOf("forme") >= 0) return "Orthographe à revoir";
  if (tags.indexOf("son") >= 0) return "Écoute à affiner";
  return "À reprendre";
}

function dictationExpectedForExercise(exercise) {
  const expected = (exercise && exercise.expected) || {};
  return expected.kr || expected.model || expected.answer || expected.text || "";
}
