#!/usr/bin/env node
/**
 * Reports which keys from src/i18n/locales/de are referenced in application code.
 *
 * - Loads the same namespaces as src/i18n/index.js (translation bundle + legal-* bundles).
 * - Parses static t("...") / t('...') calls and maps useTranslation() aliases to namespaces.
 * - Legal text is duplicated: separate namespaces (e.g. legal-agb-delivery) AND embedded under
 *   translation (legalAgbDelivery.*). When a separate-namespace key is used, the embedded
 *   translation::* copy is treated as used too.
 * - Known dynamic patterns from AGB.js, Datenschutz.js, Impressum.js are expanded so those
 *   keys are not falsely listed as unused.
 *
 * Usage: node scripts/check-i18n-keys.mjs
 *        node scripts/check-i18n-keys.mjs --json
 */

import { readFile, readdir } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.join(__dirname, '..');
const SRC = path.join(ROOT, 'src');
const LOCALE_DE = path.join(SRC, 'i18n', 'locales', 'de');

const SEPARATE_NAMESPACES = [
  'legal-agb-delivery',
  'legal-agb-payment',
  'legal-agb-consumer',
  'legal-datenschutz-basic',
  'legal-datenschutz-customer',
  'legal-datenschutz-google-orders',
  'legal-datenschutz-newsletter',
  'legal-datenschutz-chatbot',
  'legal-datenschutz-push',
  'legal-datenschutz-cookies-payment',
  'legal-datenschutz-rights',
  'legal-impressum',
  'legal-widerruf',
  'legal-batterie',
];

/** Separate i18n namespace -> key prefix inside translation bundle (locales/de/index.js). */
const NS_TO_EMBEDDED_PREFIX = {
  'legal-agb-delivery': 'legalAgbDelivery',
  'legal-agb-payment': 'legalAgbPayment',
  'legal-agb-consumer': 'legalAgbConsumer',
  'legal-datenschutz-basic': 'legalDatenschutzBasic',
  'legal-datenschutz-customer': 'legalDatenschutzCustomer',
  'legal-datenschutz-google-orders': 'legalDatenschutzGoogleOrders',
  'legal-datenschutz-newsletter': 'legalDatenschutzNewsletter',
  'legal-datenschutz-chatbot': 'legalDatenschutzChatbot',
  'legal-datenschutz-cookies-payment': 'legalDatenschutzCookiesPayment',
  'legal-datenschutz-rights': 'legalDatenschutzRights',
};

/**
 * Keys reached only via t(`…${var}…`) in a few pages — expand so they count as used.
 */
function addKnownDynamicLegalKeys(used) {
  for (let n = 1; n <= 14; n++) {
    used.add(keySet('legal-agb-delivery', `deliveryTerms.${n}`));
  }
  for (const n of [1, 2, 3, 5, 6, 7, 8]) {
    used.add(keySet('legal-agb-consumer', `distanceSelling.sections.${n}.title`));
    used.add(keySet('legal-agb-consumer', `distanceSelling.sections.${n}.content`));
  }
  for (const section of ['informationDeletion', 'serverLogfiles']) {
    used.add(keySet('legal-datenschutz-basic', `sections.${section}.title`));
    used.add(keySet('legal-datenschutz-basic', `sections.${section}.content`));
  }
  for (const section of ['operator', 'contact', 'vatId', 'disclaimer', 'copyright']) {
    used.add(keySet('legal-impressum', `sections.${section}.title`));
    used.add(keySet('legal-impressum', `sections.${section}.content`));
  }
}

/** If legal-agb-delivery::foo is used, translation::legalAgbDelivery.foo is the same strings. */
function propagateEmbeddedCopies(used) {
  const additions = [];
  for (const entry of used) {
    const sep = entry.indexOf('::');
    if (sep === -1) continue;
    const ns = entry.slice(0, sep);
    const keyPath = entry.slice(sep + 2);
    const prefix = NS_TO_EMBEDDED_PREFIX[ns];
    if (!prefix) continue;
    additions.push(keySet('translation', `${prefix}.${keyPath}`));
  }
  for (const a of additions) used.add(a);
}

function flattenLeaves(obj, prefix = '') {
  const keys = [];
  if (obj === null || obj === undefined) return keys;
  if (typeof obj !== 'object' || Array.isArray(obj)) {
    if (prefix) keys.push(prefix);
    return keys;
  }
  const entries = Object.entries(obj);
  if (entries.length === 0 && prefix) keys.push(prefix);
  for (const [k, v] of entries) {
    const next = prefix ? `${prefix}.${k}` : k;
    if (v !== null && typeof v === 'object' && !Array.isArray(v)) {
      keys.push(...flattenLeaves(v, next));
    } else {
      keys.push(next);
    }
  }
  return keys;
}

function keySet(ns, keyPath) {
  return `${ns}::${keyPath}`;
}

async function loadDefinedKeys() {
  const translationMod = await import(
    pathToFileUrl(path.join(LOCALE_DE, 'index.js'))
  );
  const translation = translationMod.default;
  const defined = new Map();
  defined.set('translation', new Set(flattenLeaves(translation)));

  for (const ns of SEPARATE_NAMESPACES) {
    const mod = await import(pathToFileUrl(path.join(LOCALE_DE, `${ns}.js`)));
    defined.set(ns, new Set(flattenLeaves(mod.default)));
  }

  return defined;
}

function pathToFileUrl(p) {
  const normalized = path.resolve(p);
  return new URL(`file://${normalized}`).href;
}

async function collectSourceFiles(dir, out = []) {
  const entries = await readdir(dir, { withFileTypes: true });
  for (const e of entries) {
    const full = path.join(dir, e.name);
    if (e.isDirectory()) {
      if (full.includes(`${path.sep}i18n${path.sep}locales`)) continue;
      await collectSourceFiles(full, out);
    } else if (/\.(jsx?|tsx?)$/.test(e.name)) {
      out.push(full);
    }
  }
  return out;
}

/**
 * Per file: map translation function alias -> i18next namespace.
 */
function buildAliasMap(source) {
  const map = new Map();

  const named = /const\s*\{\s*t:\s*(\w+)\s*\}\s*=\s*useTranslation\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
  let m;
  while ((m = named.exec(source))) {
    map.set(m[1], m[2]);
  }

  const tWithNs =
    /const\s*\{\s*t\s*\}\s*=\s*useTranslation\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
  while ((m = tWithNs.exec(source))) {
    map.set('t', m[1]);
  }

  const tDefault = /const\s*\{\s*t\s*\}\s*=\s*useTranslation\s*\(\s*\)/g;
  while (tDefault.exec(source)) {
    map.set('t', 'translation');
  }

  // withTranslation HOC: t comes from props (any position in destructuring)
  const tFromThisProps =
    /const\s*\{[^}]*\bt(?:\s*:\s*(\w+))?\b[^}]*\}\s*=\s*this\.props/;
  const mProps = tFromThisProps.exec(source);
  if (mProps) {
    map.set(mProps[1] || 't', 'translation');
  }
  const tFromProps =
    /const\s*\{[^}]*\bt(?:\s*:\s*(\w+))?\b[^}]*\}\s*=\s*props\b/;
  const mP = tFromProps.exec(source);
  if (mP) {
    map.set(mP[1] || 't', 'translation');
  }

  if (!map.has('t')) {
    map.set('t', 'translation');
  }

  return map;
}

function escapeRe(s) {
  return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

/**
 * Extract static keys from t-like calls for known aliases.
 */
function extractUsedKeysFromSource(source, filePath) {
  const aliasMap = buildAliasMap(source);
  const used = new Set();
  const dynamicHints = [];
  let m;

  const propsT =
    /(?:this\.props\.t|props\.t)\(\s*['"]([^'"]+)['"]/g;
  while ((m = propsT.exec(source))) {
    used.add(keySet('translation', m[1]));
  }

  for (const [alias, ns] of aliasMap) {
    const re = new RegExp(
      `\\b${escapeRe(alias)}\\(\\s*['"]([^'"]+)['"]`,
      'g'
    );
    while ((m = re.exec(source))) {
      used.add(keySet(ns, m[1]));
    }
  }

  const tplStatic = /\b(\w+)\(\s*`([^`${}]*)`\s*\)/g;
  while ((m = tplStatic.exec(source))) {
    const alias = m[1];
    const key = m[2].trim();
    if (!key || !aliasMap.has(alias)) continue;
    if (/^[a-zA-Z][a-zA-Z0-9_.]*$/.test(key)) {
      used.add(keySet(aliasMap.get(alias), key));
    }
  }

  if (
    /(?:this\.props\.t|props\.t|\bt\w*)\(\s*`[^`]*\$\{/.test(source)
  ) {
    dynamicHints.push(filePath);
  }

  return { used, dynamicHints };
}

function mergeSets(into, from) {
  for (const x of from) into.add(x);
}

async function main() {
  const jsonOut = process.argv.includes('--json');

  const defined = await loadDefinedKeys();
  const files = await collectSourceFiles(SRC);

  const allUsed = new Set();
  const dynamicFiles = new Set();

  for (const file of files) {
    const source = await readFile(file, 'utf8');
    const { used, dynamicHints } = extractUsedKeysFromSource(source, file);
    mergeSets(allUsed, used);
    if (dynamicHints.length) dynamicFiles.add(file);
  }

  addKnownDynamicLegalKeys(allUsed);
  propagateEmbeddedCopies(allUsed);

  const unusedByNs = new Map();
  let totalDefined = 0;
  let totalUnused = 0;

  for (const [ns, keys] of defined) {
    const unused = [];
    for (const k of keys) {
      totalDefined++;
      const full = keySet(ns, k);
      if (!allUsed.has(full)) {
        unused.push(k);
        totalUnused++;
      }
    }
    if (unused.length) unusedByNs.set(ns, unused.sort());
  }

  const orphans = new Set();
  for (const u of allUsed) {
    const [ns, keyPath] = u.split('::');
    const set = defined.get(ns);
    if (!set || !set.has(keyPath)) {
      orphans.add(u);
    }
  }

  if (jsonOut) {
    console.log(
      JSON.stringify(
        {
          totalDefined,
          totalUsed: allUsed.size,
          totalUnused,
          unusedByNamespace: Object.fromEntries(unusedByNs),
          usedButNotDefined: [...orphans].sort(),
          filesWithLikelyDynamicT: [...dynamicFiles].map((f) =>
            path.relative(ROOT, f)
          ),
        },
        null,
        2
      )
    );
    return;
  }

  console.log('i18n key usage (locale: de)\n');
  console.log(
    'Legal strings exist twice: separate namespaces (AGB, Datenschutz, …) and embedded copies'
  );
  console.log(
    'under translation (legalAgbDelivery.*, …). The latter are marked used when the former are.\n'
  );
  console.log(`Defined keys (all namespaces): ${totalDefined}`);
  console.log(`References after static scan + known dynamic legal patterns: ${allUsed.size}`);
  console.log(`Unused (best-effort): ${totalUnused}`);
  console.log(`Used but not in locale files: ${orphans.size}\n`);

  const stillDynamic = [...dynamicFiles].filter(
    (f) =>
      ![
        'pages/AGB.js',
        'pages/Datenschutz.js',
        'pages/Impressum.js',
      ].includes(path.relative(SRC, f).replace(/\\/g, '/'))
  );
  if (stillDynamic.length) {
    console.log(
      'Other files with t(`…${…}…`) (may still hide unused keys — not expanded):'
    );
    for (const f of stillDynamic.sort()) {
      console.log(`  ${path.relative(ROOT, f)}`);
    }
    console.log('');
  }

  for (const [ns, keys] of [...unusedByNs.entries()].sort((a, b) =>
    a[0].localeCompare(b[0])
  )) {
    console.log(`--- ${ns} (${keys.length} unused) ---`);
    for (const k of keys) {
      console.log(`  ${k}`);
    }
    console.log('');
  }

  if (orphans.size) {
    console.log('--- Referenced in code but missing from de locale ---');
    for (const o of [...orphans].sort()) {
      console.log(`  ${o.replace('::', ' / ')}`);
    }
  }

  console.log(
    '\nNote: Keys built from arbitrary variables or unknown dynamic templates may still look unused.'
  );
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});
