#!/usr/bin/env node
// Walk every entry in src/content/** and flag any localized field whose
// `en` and `pt` values are identical. Identical pairs are how a bilingual
// site quietly becomes monolingual — the schema accepts them, the build
// passes, and a Portuguese speaker sees English.
//
//   node .agents/scripts/audit-translations.mjs                 # walks src/content
//   node .agents/scripts/audit-translations.mjs src/content/    # explicit root
//
// Exits non-zero if any unallowed pair is identical. The output is grouped
// by collection, then by file, then by field path, so the report reads like
// a translation backlog rather than a wall of strings.
//
// This is the sibling of extract-strings.mjs: that one proves the
// migration moved every string; this one proves every string actually
// differs across locales.
//
// Allowlist: a sibling `.agents/scripts/audit-translations.allowlist.json`
// lists (collection, file, field) tuples that are intentionally identical
// across locales (numeric card labels, product nouns, model-tier series
// names, etc.). Entries in the allowlist are listed under "ALLOWED" in the
// output and do not affect the exit code. Adding to the allowlist is a
// deliberate edit + commit; future translators can see it and understand
// which identicals are intentional.

import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
import { dirname, join, relative } from 'node:path';
import { fileURLToPath } from 'node:url';

const ROOT = process.argv[2] ?? 'src/content';

const walk = (dir) =>
  readdirSync(dir).flatMap((name) => {
    const path = join(dir, name);
    return statSync(path).isDirectory() ? walk(path) : [path];
  });

const files = walk(ROOT).filter((path) => path.endsWith('.json'));

// Returns an array of [fieldPath, en, pt] tuples for a parsed document.
// Handles the two shapes in the project:
//   1. { ..., someKey: { en, pt }, ... }    — the `localized` Zod helper
//   2. { en: { ...string IDs... }, pt: { ...string IDs... } }   — rules/copy.json
const collect = (node, fieldPath = '', out = []) => {
  if (!node || typeof node !== 'object') return out;

  if ('en' in node && 'pt' in node) {
    out.push([fieldPath || '(root)', node.en, node.pt]);
    return out;
  }

  for (const [key, value] of Object.entries(node)) {
    const next = fieldPath ? `${fieldPath}.${key}` : key;
    // Top-level `{ en: {...}, pt: {...} }` — recurse into each side.
    if (
      (key === 'en' || key === 'pt') &&
      value &&
      typeof value === 'object' &&
      !Array.isArray(value)
    ) {
      collect(value, '', out);
    } else {
      collect(value, next, out);
    }
  }
  return out;
};

const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);

// Compute the collection name from the path relative to ROOT.
//   src/content/chapters/landing.json  ->  chapters
//   src/content/rules/copy.json         ->  rules
const collectionOf = (file, root) => {
  const rel = relative(root, file);
  const parts = rel.split('/');
  // ['chapters', 'landing.json'] -> 'chapters'
  return parts.length >= 2 ? parts[0] : '(root)';
};

// Load the allowlist. The file lives next to this script and lists
// intentional identicals (numeric labels, product nouns, model-tier series
// names, etc.). Missing file is OK — the audit still runs.
const here = dirname(fileURLToPath(import.meta.url));
const allowlistPath = join(here, 'audit-translations.allowlist.json');
const allowlist = new Set();
const allowlistReasons = new Map();

if (existsSync(allowlistPath)) {
  const { entries } = JSON.parse(readFileSync(allowlistPath, 'utf8'));
  for (const { collection, file, field, reason } of entries) {
    allowlist.add(`${collection}${file}${field}`);
    allowlistReasons.set(`${collection}${file}${field}`, reason);
  }
}

const groups = new Map();
const allowedGroups = new Map();
let totalLocalized = 0;
let totalReal = 0;
let totalAllowed = 0;

for (const file of files) {
  let data;
  try {
    data = JSON.parse(readFileSync(file, 'utf8'));
  } catch (err) {
    console.error(`skip: ${file} is not valid JSON (${err.message})`);
    continue;
  }
  const pairs = collect(data);
  if (pairs.length === 0) continue;

  const collection = collectionOf(file, ROOT);
  const filename = relative(ROOT, file);
  // Allowlist entries key on the file's name within its collection
  // (e.g. `landing.json`), so strip the collection prefix from
  // `filename` when matching. The full relative path is still used
  // for the report display.
  const collectionFile = filename.startsWith(collection + '/')
    ? filename.slice(collection.length + 1)
    : filename;

  // Split identical pairs into real offenders (fail the gate) and
  // allowlisted ones (listed under ALLOWED, do not affect exit code).
  const real = [];
  const allowed = [];
  for (const [field, en, pt] of pairs) {
    if (!same(en, pt)) continue;
    const key = `${collection}${collectionFile}${field}`;
    if (allowlist.has(key)) {
      allowed.push([field, en, allowlistReasons.get(key)]);
    } else {
      real.push([field, en, null]);
    }
  }

  totalLocalized += pairs.length;

  if (real.length > 0) {
    totalReal += real.length;
    if (!groups.has(collection)) groups.set(collection, []);
    groups.get(collection).push({ file: filename, pairs: real });
  }

  if (allowed.length > 0) {
    totalAllowed += allowed.length;
    if (!allowedGroups.has(collection)) allowedGroups.set(collection, []);
    allowedGroups.get(collection).push({ file: filename, pairs: allowed });
  }
}

const printGroup = (entries, label) => {
  for (const [collection, list] of entries) {
    console.log(`\n[${collection}]`);
    for (const { file, pairs } of list) {
      console.log(`  ${file}`);
      for (const [field, en, reason] of pairs) {
        const sample = typeof en === 'string' ? JSON.stringify(en).slice(0, 80) : '<non-string>';
        const suffix = reason ? ` [${label}: ${reason}]` : '';
        console.log(`    - ${field.padEnd(28)} ${sample}${suffix}`);
      }
    }
  }
};

// Clean case: no identicals at all.
if (totalReal === 0 && totalAllowed === 0) {
  console.log('OK: no identical en/pt pairs found under', ROOT);
  process.exit(0);
}

// Show allowed entries first, then real offenders (if any).
printGroup(allowedGroups, 'ALLOWED');

if (totalReal > 0) {
  printGroup(groups, 'REAL');
  console.log(
    `\nFAIL: ${totalReal} unallowed identical localized field(s) across ${groups.size} collection(s) of ${ROOT}`,
  );
  console.log(
    'A `pt` identical to `en` is how a bilingual site becomes monolingual. Translate, then re-run.',
  );
  process.exit(1);
}

console.log(`\nOK: ${totalAllowed} identical field(s) found, all on the allowlist.`);
process.exit(0);
