#!/usr/bin/env node
// Regenerate src/models/iso_codes.rs from the IANA Language Subtag Registry.
//
// Usage: node tools/gen-locales.mjs
//
// Sources:
//   data/registry/registry.json   Mirror of https://github.com/mattcg/language-subtag-registry
//                                 (the canonical IANA registry as JSON). Drives Market and Lang.
//   data/ms-store-locales.csv     Curated BCP-47 tags accepted by the Microsoft Store, lifted
//                                 from learn.microsoft.com's "Supported languages" table.
//                                 Drives LanguageTag.
//
// Emits three enums:
//   Market       - region subtags, 2-letter alpha, ISO 3166-1 alpha-2
//   Lang         - language subtags, 2-letter, ISO 639-1
//   LanguageTag  - BCP-47 tags the Microsoft Store accepts in app manifests
// Deprecated and private-use registry entries are skipped.

import { promises as fs } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..');

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

const pascal = code => code[0].toUpperCase() + code.slice(1).toLowerCase();
const pascalTag = tag => tag.split('-').map(pascal).join('');
const escape = s => s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');

// ---------------------------------------------------------------------------
// Source loading
// ---------------------------------------------------------------------------

const registry = JSON.parse(
    await fs.readFile(join(repoRoot, 'data/registry/registry.json'), 'utf8'),
);

const isPrivateUse = entry =>
    Array.isArray(entry.Description) &&
    entry.Description.some(d => /private use/i.test(d));

const languages = registry
    .filter(e =>
        e.Type === 'language' &&
        /^[a-z]{2}$/.test(e.Subtag) &&
        !e.Deprecated &&
        !isPrivateUse(e),
    )
    .map(e => ({
        variant: pascal(e.Subtag),
        display: e.Subtag,
        key: e.Subtag,
        label: e.Description[0],
    }))
    .sort((a, b) => a.key.localeCompare(b.key));

const regions = registry
    .filter(e =>
        e.Type === 'region' &&
        /^[A-Z]{2}$/.test(e.Subtag) &&
        !e.Deprecated &&
        !isPrivateUse(e),
    )
    .map(e => ({
        variant: pascal(e.Subtag),
        display: e.Subtag,
        key: e.Subtag.toLowerCase(),
        label: e.Description[0],
    }))
    .sort((a, b) => a.display.localeCompare(b.display));

const langKeys = new Set(languages.map(l => l.key));
const regionKeys = new Set(regions.map(r => r.display)); // already uppercase

function subtagsOf(tag) {
    const parts = tag.split('-');
    const primary = parts[0].toLowerCase();
    let region = null;
    for (let i = 1; i < parts.length; i++) {
        if (/^[A-Za-z]{2}$/.test(parts[i])) {
            region = parts[i].toUpperCase();
            break;
        }
    }
    return { primary, region };
}

const storeCsv = await fs.readFile(join(repoRoot, 'data/ms-store-locales.csv'), 'utf8');
const tags = storeCsv
    .split(/\r?\n/)
    .slice(1)
    .map(line => line.trim())
    .filter(Boolean)
    .map(line => {
        const m = line.match(/^"([^"]+)"\s*,\s*"([^"]*)"/);
        if (!m) throw new Error(`unparsable CSV line: ${line}`);
        const [, tag, label] = m;
        const { primary, region } = subtagsOf(tag);
        return {
            variant: pascalTag(tag),
            display: tag,
            key: tag.toLowerCase(),
            label,
            lang: langKeys.has(primary) ? pascal(primary) : null,
            market: region && regionKeys.has(region) ? pascal(region) : null,
        };
    })
    .sort((a, b) => a.key.localeCompare(b.key));

// ---------------------------------------------------------------------------
// Emitter
// ---------------------------------------------------------------------------

function emitEnum(out, name, entries, doc) {
    out.push(`/// ${doc}`);
    out.push('///');
    out.push('/// Auto-generated by `tools/gen-locales.mjs` — do not edit by hand.');
    out.push('#[allow(non_camel_case_types)]');
    out.push('#[derive(Debug, Clone, PartialEq, Eq)]');
    out.push(`pub enum ${name} {`);
    for (const e of entries) out.push(`    ${e.variant},`);
    out.push('}');
    out.push('');

    out.push(`impl ${name} {`);
    out.push('    /// Returns the canonical code string.');
    out.push('    pub fn as_str(&self) -> &\'static str {');
    out.push('        match self {');
    for (const e of entries) {
        out.push(`            ${name}::${e.variant} => "${escape(e.display)}",`);
    }
    out.push('        }');
    out.push('    }');
    out.push('');
    out.push('    /// Returns the English name from the registry.');
    out.push('    pub fn english_name(&self) -> &\'static str {');
    out.push('        match self {');
    for (const e of entries) {
        out.push(`            ${name}::${e.variant} => "${escape(e.label)}",`);
    }
    out.push('        }');
    out.push('    }');
    out.push('');
    out.push('    /// Iterates over every variant in code order.');
    out.push(`    pub fn all() -> &'static [${name}] {`);
    out.push(`        &[${entries.map(e => `${name}::${e.variant}`).join(', ')}]`);
    out.push('    }');
    out.push('}');
    out.push('');

    out.push(`impl FromStr for ${name} {`);
    out.push('    type Err = String;');
    out.push('    fn from_str(s: &str) -> Result<Self, Self::Err> {');
    out.push('        match s.to_ascii_lowercase().as_str() {');
    for (const e of entries) {
        out.push(`            "${escape(e.key)}" => Ok(${name}::${e.variant}),`);
    }
    out.push(`            _ => Err(format!("unknown ${name.toLowerCase()}: {s}")),`);
    out.push('        }');
    out.push('    }');
    out.push('}');
    out.push('');

    out.push(`impl serde::Serialize for ${name} {`);
    out.push('    fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {');
    out.push('        ser.serialize_str(self.as_str())');
    out.push('    }');
    out.push('}');
    out.push('');
    out.push(`impl<'de> serde::Deserialize<'de> for ${name} {`);
    out.push("    fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {");
    out.push("        let s = <std::borrow::Cow<'_, str>>::deserialize(de)?;");
    out.push(`        <${name} as FromStr>::from_str(&s).map_err(serde::de::Error::custom)`);
    out.push('    }');
    out.push('}');
    out.push('');
}

// ---------------------------------------------------------------------------
// Write
// ---------------------------------------------------------------------------

const out = [];
out.push('// Auto-generated by `tools/gen-locales.mjs`.');
out.push('// Sources:');
out.push('//   data/registry/registry.json   - IANA Language Subtag Registry (mattcg mirror)');
out.push('//   data/ms-store-locales.csv     - Microsoft Store supported BCP-47 tags');
out.push('// Re-run the generator after refreshing either file.');
out.push('//');
out.push(`// Regions: ${regions.length}    Languages: ${languages.length}    Store tags: ${tags.length}`);
out.push('');
out.push('use std::str::FromStr;');
out.push('');

emitEnum(out, 'Market', regions, 'A Microsoft Store market, identified by an ISO 3166-1 alpha-2 region subtag.');
emitEnum(out, 'Lang', languages, 'A Microsoft Store UI language, identified by an ISO 639-1 alpha-2 subtag.');
emitEnum(out, 'LanguageTag', tags, 'A BCP-47 language tag accepted by the Microsoft Store (e.g. `en-US`, `zh-Hant`, `sr-Cyrl-RS`).');

// Tag → Lang / Market projections used by `Locale::from_tag`.
out.push('impl LanguageTag {');
out.push('    /// Primary language subtag, if it maps to an ISO 639-1 [`Lang`].');
out.push('    /// Returns `None` for tags whose primary subtag is ISO 639-2 / 639-3');
out.push('    /// (e.g. `chr-Cher`, `fil-PH`).');
out.push('    pub fn lang(&self) -> Option<Lang> {');
out.push('        match self {');
for (const t of tags) {
    const val = t.lang ? `Some(Lang::${t.lang})` : 'None';
    out.push(`            LanguageTag::${t.variant} => ${val},`);
}
out.push('        }');
out.push('    }');
out.push('');
out.push('    /// Region subtag, if present and resolvable to an ISO 3166-1 alpha-2 [`Market`].');
out.push('    /// Returns `None` for tags with no region (`en`, `zh-Hant`) or with a');
out.push('    /// UN M.49 numeric region (`en-053`, `es-419`).');
out.push('    pub fn region(&self) -> Option<Market> {');
out.push('        match self {');
for (const t of tags) {
    const val = t.market ? `Some(Market::${t.market})` : 'None';
    out.push(`            LanguageTag::${t.variant} => ${val},`);
}
out.push('        }');
out.push('    }');
out.push('}');
out.push('');

const target = join(repoRoot, 'src/models/iso_codes.rs');
await fs.writeFile(target, out.join('\n'));
console.log(`Wrote ${target}: ${regions.length} regions, ${languages.length} languages, ${tags.length} store tags`);
