#!/usr/bin/env node
/**
 * i18n-vakt: fäller bygget om översättningarna inte är kompletta eller om det
 * finns hårdkodad text i UI:t. Kör: `pnpm --filter @app/mobile i18n:check`.
 *
 * Poängen (långsiktigt): när ett nytt språk läggs till behöver ingen leta
 * skärm för skärm – vakten listar exakt vilka nycklar som saknas, och hindrar
 * att ny hårdkodad svenska smyger in. Wire:a in den i CI så kan bygget aldrig
 * gå live med halvfärdig lokalisering.
 *
 * FEL (exit 1):
 *   1. Ett språk saknar en nyckel som svenska (källan) har  → faller till svenska.
 *   2. En använd t("nyckel") saknas i svenska.
 *   3. Hårdkodad användartext (svenska diakriter, eller råsträng i label/
 *      placeholder/title/text/Alert/setError som inte går via t()).
 * VARNING (exit 0): språk med extra nycklar som svenska saknar (t.ex. pl-plural).
 *
 * Undanta enskild rad med kommentaren  // i18n-ignore
 */
import { readdirSync, readFileSync, statSync } from "node:fs";
import { join, relative } from "node:path";

const ROOT = process.cwd(); // apps/mobile
const LOCALES_DIR = join(ROOT, "src/locales");
const SRC_DIR = join(ROOT, "src");
const SOURCE_LANG = "sv";

// Filer/mönster som INTE är UI-text: locale-data och enhetsvokabulär per språk.
const SKIP_FILES = [/src\/locales\//, /src\/lib\/i18n\.ts$/, /src\/lib\/units\.ts$/];
// Råsträngar som är legitima (varumärke, format, tekniska värden).
const ALLOW = [
  /^[\s\p{Emoji}\p{P}\p{S}·•⏳⭐★☆✍️🍽️🥣🥗🥐🍎🍰🧺🧠🛒👥⚙️❄️🗄️🥕→↔↩︎▸▾⚠︎%–—-]+$/u, // bara symboler/emoji
  /^https?:\/\//,
  /Cibello/,
];
// Ord som är identiska/universella på alla målspråk – ingen översättning behövs.
const ALLOW_EXACT = new Set(["OK", "kcal", "g", "kg", "ml", "dl", "l", "st"]);

/** Ta bort kommentarer men BEHÅLL radnummer (ersätt med blanksteg, ej borttag). */
function stripComments(src) {
  const keepNL = (m) => m.replace(/[^\n]/g, " ");
  return src
    .replace(/\{\/\*[\s\S]*?\*\/\}/g, keepNL) // {/* JSX-kommentar */}
    .replace(/\/\*[\s\S]*?\*\//g, keepNL) // /* blockkommentar */
    .split("\n")
    .map((line) => line.replace(/([^:"'`\\])\/\/.*$/, "$1").replace(/^\s*\/\/.*$/, "")) // // radslut (ej URL)
    .join("\n");
}

function walk(dir) {
  const out = [];
  for (const name of readdirSync(dir)) {
    const p = join(dir, name);
    const st = statSync(p);
    if (st.isDirectory()) out.push(...walk(p));
    else if (/\.(tsx|ts)$/.test(p)) out.push(p);
  }
  return out;
}

function loadLocales() {
  const langs = readdirSync(LOCALES_DIR).filter((d) =>
    statSync(join(LOCALES_DIR, d)).isDirectory(),
  );
  const map = {};
  for (const lang of langs) {
    map[lang] = JSON.parse(readFileSync(join(LOCALES_DIR, lang, "common.json"), "utf8"));
  }
  return map;
}

const errors = [];
const warnings = [];

// ---- 1. Nyckel-paritet ----------------------------------------------------
const locales = loadLocales();
const svKeys = new Set(Object.keys(locales[SOURCE_LANG]));
for (const [lang, data] of Object.entries(locales)) {
  if (lang === SOURCE_LANG) continue;
  const keys = new Set(Object.keys(data));
  const missing = [...svKeys].filter((k) => !keys.has(k));
  const extra = [...keys].filter((k) => !svKeys.has(k));
  if (missing.length)
    errors.push(
      `[${lang}] saknar ${missing.length} nyckel/nycklar som ${SOURCE_LANG} har (faller till svenska):\n    ` +
        missing.slice(0, 30).join("\n    ") +
        (missing.length > 30 ? `\n    …(+${missing.length - 30})` : ""),
    );
  if (extra.length)
    warnings.push(
      `[${lang}] har ${extra.length} extra nyckel/nycklar (oanvända?): ${extra.slice(0, 8).join(", ")}${extra.length > 8 ? "…" : ""}`,
    );
}

// ---- 2 & 3. Använda nycklar + hårdkodad text ------------------------------
const files = walk(SRC_DIR).filter((f) => !SKIP_FILES.some((re) => re.test(f)));
const usedStatic = new Set();
let dynamicUses = 0;
const DIACRITIC = /[åäöÅÄÖ]/;
// t("nyckel") / t('nyckel') / t(`nyckel`)  – statisk nyckel utan ${}
const T_STATIC = /\bt\(\s*["'`]([^"'`$}{]+)["'`]/g;
// t(`...${...}`) – dynamisk nyckel, hoppa men räkna
const T_DYNAMIC = /\bt\(\s*`[^`]*\$\{/g;
// användarnära positioner med RÅSTRÄNG (inte {t(...)})
const POS =
  /(?:label|placeholder|title|text|header|message)\s*[=:]\s*(["'`])((?:(?!\1).)*[A-Za-zÅÄÖåäö]{2,}(?:(?!\1).)*)\1/g;
const ALERT =
  /(?:Alert\.alert|setError|EmptyState\s+text=)\s*\(?\s*(["'`])((?:(?!\1).)*[A-Za-zÅÄÖåäö]{2,}(?:(?!\1).)*)\1/g;
// JSX-text: >Svensk text<  (fångar diakriter i textnoder)
const JSXTEXT = />\s*([^<>{}\n]*[åäöÅÄÖ][^<>{}\n]*?)\s*</g;

const allowed = (s) => ALLOW_EXACT.has(s.trim()) || ALLOW.some((re) => re.test(s.trim()));

for (const file of files) {
  const rel = relative(ROOT, file);
  const raw = readFileSync(file, "utf8");
  const rawLines = raw.split("\n");
  const src = stripComments(raw);
  let m;
  while ((m = T_STATIC.exec(src))) usedStatic.add(m[1]);
  dynamicUses += (src.match(T_DYNAMIC) || []).length;

  const lines = src.split("\n");
  lines.forEach((line, i) => {
    const trimmed = line.trim();
    if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) return;
    // i18n-ignore läses på RÅraden (kommentaren strippas ju bort i `line`).
    if (/i18n-ignore/.test(rawLines[i] ?? "")) return;
    const hits = new Set();
    for (const re of [POS, ALERT]) {
      re.lastIndex = 0;
      let mm;
      while ((mm = re.exec(line))) {
        const val = mm[2];
        if (val.includes("${")) continue; // ren interpolationsdel
        if (!allowed(val)) hits.add(val);
      }
    }
    JSXTEXT.lastIndex = 0;
    let jm;
    while ((jm = JSXTEXT.exec(line))) {
      const val = jm[1];
      if (val.includes("{") || val.includes("`")) continue;
      if (!allowed(val)) hits.add(val);
    }
    // diakriter i råsträngar OCH mallsträngar (även med ${…} – svensk text runt
    // interpolationen behöver ändå gå via t() med parametrar).
    if (DIACRITIC.test(line)) {
      const strs = line.match(/"[^"\n]*[åäöÅÄÖ][^"\n]*"|`[^`\n]*[åäöÅÄÖ][^`\n]*`/g) || [];
      for (const s of strs) {
        const val = s.slice(1, -1);
        const bare = val.replace(/\$\{[^}]*\}/g, ""); // ta bort interpolation, kolla resten
        if (DIACRITIC.test(bare) && !allowed(val)) hits.add(val);
      }
    }
    // fristående sträng-/mallliteral på egen rad (multi-rad call-argument, t.ex.
    // `Bild ${count} tagen`) – en fras (innehåller mellanslag) är nästan alltid UI-text.
    const st = trimmed.match(/^(["'`])((?:(?!\1).)*)\1,?$/);
    if (st) {
      const val = st[2];
      const bare = val.replace(/\$\{[^}]*\}/g, "");
      if (/\s/.test(bare) && /[A-Za-zÅÄÖåäö]{2,}/.test(bare) && !allowed(val)) hits.add(val);
    }
    for (const h of hits) errors.push(`[hårdkodad] ${rel}:${i + 1}  «${h.slice(0, 60)}»`);
  });
}

// använda nycklar måste finnas i svenska (plural: tillåt _one/_other-varianter)
const missingUsed = [...usedStatic].filter(
  (k) => !svKeys.has(k) && !svKeys.has(`${k}_other`) && !svKeys.has(`${k}_one`),
);
if (missingUsed.length)
  errors.push(
    `Använda t()-nycklar som saknas i ${SOURCE_LANG}:\n    ` + missingUsed.join("\n    "),
  );

// ---- Rapport --------------------------------------------------------------
const langCount = Object.keys(locales).length;
console.log(
  `i18n-vakt: ${langCount} språk, ${svKeys.size} nycklar, ${usedStatic.size} statiska t()-anrop, ${dynamicUses} dynamiska.`,
);
for (const w of warnings) console.log("VARNING " + w);
if (errors.length) {
  console.error(`\n✗ ${errors.length} fel:\n`);
  for (const e of errors) console.error("  " + e);
  console.error("\nÅtgärda ovan (eller markera en rad med // i18n-ignore om den är avsiktlig).");
  process.exit(1);
}
console.log(
  "✓ i18n komplett: alla språk har alla nycklar, inga saknade nycklar, ingen hårdkodad UI-text.",
);
