/// Generates lib/data/iso_639_data.dart from the curated offline ISO 639 catalog.
///
/// Usage:
///   dart run scripts/generate_iso_639_data.dart [input.json] [output.dart]
library;

import 'dart:convert';
import 'dart:io';

const defaultIso639Input = 'scripts/data/iso_639_codes.json';
const defaultIso639Output = 'lib/data/iso_639_data.dart';

final class Iso639CatalogEntry {
  const Iso639CatalogEntry({
    required this.primary,
    required this.terminology,
    required this.bibliographic,
    required this.name,
  });

  final String primary;
  final String terminology;
  final String? bibliographic;
  final String name;
}

final class Iso639Catalog {
  const Iso639Catalog({required this.entries});

  final List<Iso639CatalogEntry> entries;
}

typedef AtomicFileWriter = Future<void> Function(String path, String contents);

Iso639Catalog parseIso639Catalog(String source) {
  final Object? decoded;
  try {
    decoded = jsonDecode(source);
  } on FormatException catch (error) {
    throw FormatException('Invalid ISO 639 catalog JSON: ${error.message}');
  }

  final root = _expectMap(decoded, 'catalog');
  if (root['schemaVersion'] != 1) {
    throw const FormatException('ISO 639 catalog schemaVersion must be 1');
  }

  final languages = _expectMap(root['languages'], 'languages');
  if (languages.isEmpty) {
    throw const FormatException('ISO 639 catalog languages must not be empty');
  }

  final entries = <Iso639CatalogEntry>[];
  final allCodes = <String>{};
  String? previousPrimary;

  for (final catalogEntry in languages.entries) {
    final key = catalogEntry.key;
    if (!_isPrimaryCode(key)) {
      throw FormatException('Language key must be exactly two lowercase letters: $key');
    }
    if (previousPrimary != null && key.compareTo(previousPrimary) <= 0) {
      throw FormatException('Language keys must be in strictly increasing order: $key');
    }
    previousPrimary = key;

    final rawEntry = _expectMap(catalogEntry.value, 'languages.$key');
    final primary = _expectString(rawEntry['primary'], 'languages.$key.primary');
    if (primary != key) {
      throw FormatException('Language key $key does not match primary code $primary');
    }
    if (!_isPrimaryCode(primary)) {
      throw FormatException('languages.$key.primary must be exactly two lowercase letters');
    }
    if (!allCodes.add(primary)) {
      throw FormatException('Duplicate ISO 639 code: $primary');
    }

    final terminology = _expectString(rawEntry['terminology'], 'languages.$key.terminology');
    if (!_isAliasCode(terminology)) {
      throw FormatException('languages.$key.terminology must be exactly three lowercase letters');
    }
    if (!allCodes.add(terminology)) {
      throw FormatException('Duplicate ISO 639 code: $terminology');
    }

    final rawBibliographic = rawEntry['bibliographic'];
    final String? bibliographic;
    if (rawBibliographic == null) {
      bibliographic = null;
    } else {
      bibliographic = _expectString(rawBibliographic, 'languages.$key.bibliographic');
      if (!_isAliasCode(bibliographic)) {
        throw FormatException('languages.$key.bibliographic must be null or exactly three lowercase letters');
      }
      if (!allCodes.add(bibliographic)) {
        throw FormatException('Duplicate ISO 639 code: $bibliographic');
      }
    }

    final name = _expectNonemptyString(rawEntry['name'], 'languages.$key.name');
    entries.add(
      Iso639CatalogEntry(primary: primary, terminology: terminology, bibliographic: bibliographic, name: name),
    );
  }

  return Iso639Catalog(entries: List.unmodifiable(entries));
}

String renderIso639Data(Iso639Catalog catalog) {
  final output = StringBuffer()
    ..writeln(
      '// Generated by dart run scripts/generate_iso_639_data.dart from '
      'scripts/data/iso_639_codes.json; do not edit by hand.',
    )
    ..writeln()
    ..writeln('class LanguageEntry {')
    ..writeln('  final String code1;')
    ..writeln('  final String code2;')
    ..writeln('  final String? code2B;')
    ..writeln('  final String name;')
    ..writeln('  const LanguageEntry(this.code1, this.code2, this.code2B, this.name);')
    ..writeln('}')
    ..writeln()
    ..writeln('const languageEntries = <String, LanguageEntry>{');

  for (final entry in catalog.entries) {
    final bibliographic = entry.bibliographic == null ? 'null' : _dartString(entry.bibliographic!);
    output.writeln(
      '  ${_dartString(entry.primary)}: LanguageEntry('
      '${_dartString(entry.primary)}, ${_dartString(entry.terminology)}, $bibliographic, ${_dartString(entry.name)}),',
    );
  }

  output
    ..writeln('};')
    ..writeln()
    ..writeln('const code2ToCode1 = <String, String>{');
  for (final entry in catalog.entries) {
    output.writeln('  ${_dartString(entry.terminology)}: ${_dartString(entry.primary)},');
  }

  output
    ..writeln('};')
    ..writeln()
    ..writeln('const code2BToCode1 = <String, String>{');
  for (final entry in catalog.entries) {
    final bibliographic = entry.bibliographic;
    if (bibliographic != null) {
      output.writeln('  ${_dartString(bibliographic)}: ${_dartString(entry.primary)},');
    }
  }
  output.writeln('};');
  return output.toString();
}

Future<void> writeFileAtomically(String path, String contents) async {
  final output = File(path);
  await output.parent.create(recursive: true);
  final temporary = File('$path.tmp.$pid.${DateTime.now().microsecondsSinceEpoch}');
  try {
    await temporary.writeAsString(contents, flush: true);
    await temporary.rename(path);
  } finally {
    if (await temporary.exists()) {
      await temporary.delete();
    }
  }
}

Future<void> generateIso639Data(
  String inputPath,
  String outputPath, {
  AtomicFileWriter writer = writeFileAtomically,
}) async {
  final source = await File(inputPath).readAsString();
  final catalog = parseIso639Catalog(source);
  final rendered = renderIso639Data(catalog);
  await writer(outputPath, rendered);
}

Future<void> main(List<String> arguments) async {
  if (arguments.length > 2) {
    stderr.writeln('Usage: dart run scripts/generate_iso_639_data.dart [input.json] [output.dart]');
    exitCode = 64;
    return;
  }

  final inputPath = arguments.isEmpty ? defaultIso639Input : arguments[0];
  final outputPath = arguments.length < 2 ? defaultIso639Output : arguments[1];
  await generateIso639Data(inputPath, outputPath);
}

Map<String, Object?> _expectMap(Object? value, String path) {
  if (value is! Map<String, Object?>) {
    throw FormatException('$path must be a JSON object');
  }
  return value;
}

String _expectString(Object? value, String path) {
  if (value is! String) {
    throw FormatException('$path must be a string');
  }
  return value;
}

String _expectNonemptyString(Object? value, String path) {
  final string = _expectString(value, path);
  if (string.trim().isEmpty) {
    throw FormatException('$path must not be empty');
  }
  return string;
}

bool _isPrimaryCode(String code) => RegExp(r'^[a-z]{2}$').hasMatch(code);
bool _isAliasCode(String code) => RegExp(r'^[a-z]{3}$').hasMatch(code);

String _dartString(String value) {
  if (value.contains("'")) {
    final escaped = value
        .replaceAll(r'\', r'\\')
        .replaceAll('"', r'\"')
        .replaceAll(r'$', r'\$')
        .replaceAll('\r', r'\r')
        .replaceAll('\n', r'\n')
        .replaceAll('\t', r'\t');
    return '"$escaped"';
  }

  final escaped = value
      .replaceAll(r'\', r'\\')
      .replaceAll("'", r"\'")
      .replaceAll(r'$', r'\$')
      .replaceAll('\r', r'\r')
      .replaceAll('\n', r'\n')
      .replaceAll('\t', r'\t');
  return "'$escaped'";
}
