const fs = require('node:fs');
const path = require('node:path');
const assert = require('node:assert/strict');
const crypto = require('node:crypto');

const abiNames = ['arm64-v8a', 'armeabi-v7a', 'x86_64'];
const macros = {
  EVENT__NUMERIC_VERSION: '0x02010d00',
  EVENT__PACKAGE_VERSION: '"2.1.13"',
  EVENT__VERSION_PATCH: '13',
  EVENT__VERSION: '"2.1.13-stable"',
};

function within(root, candidate) {
  const relative = path.relative(root, candidate);
  assert(relative && relative !== '..' && !relative.startsWith(`..${path.sep}`) &&
    !path.isAbsolute(relative), `Path escapes project: ${candidate}`);
}

function files(root, relative = '') {
  return fs.readdirSync(path.join(root, relative), {withFileTypes: true}).flatMap(entry => {
    assert(!entry.isSymbolicLink(), `Unexpected source symlink: ${entry.name}`);
    const name = path.join(relative, entry.name);
    return entry.isDirectory() ? files(root, name) : [name];
  }).sort();
}

const hash = file => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
function sameTree(source, target, sourceFiles) {
  if (!fs.existsSync(target)) return false;
  const targetFiles = files(target);
  return JSON.stringify(targetFiles) === JSON.stringify(sourceFiles) &&
    sourceFiles.every(file => hash(path.join(source, file)) === hash(path.join(target, file)));
}

function main() {
  const args = process.argv.slice(2);
  assert(args.length >= 1 && args.length <= 2 && (!args[1] || args[1] === '--check'),
    'Usage: node patch-libevent-2113.cjs <harmony-root> [--check]');
  const checkOnly = args[1] === '--check';
  const root = fs.realpathSync(args[0]);
  const source = fs.realpathSync(path.join(__dirname, '../vendor/libevent-2.1.13-src'));
  assert(/^Changes in version 2\.1\.13-stable/m.test(fs.readFileSync(path.join(source, 'ChangeLog'), 'utf8')),
    'Vendor source is not libevent 2.1.13-stable');
  assert(fs.existsSync(path.join(source, 'LICENSE')), 'Vendor LICENSE missing');
  const sourceFiles = files(source);
  const candidates = ['entry/oh_modules', 'oh_modules'].map(prefix =>
    path.join(root, prefix, '@rnoh/react-native-openharmony/src/main/cpp'));
  const dirs = [...new Set(candidates.filter(p => fs.existsSync(p)).map(p => fs.realpathSync(p)))];
  assert(dirs.length, 'Source RNOH missing; run ohpm install --all with react_native_openharmony.har');
  for (const prefix of ['entry/oh_modules/.ohpm', 'oh_modules/.ohpm']) {
    const store = path.join(root, prefix);
    if (!fs.existsSync(store)) continue;
    for (const name of fs.readdirSync(store).filter(name => name.startsWith('@rnoh+react-native-openharmony@'))) {
      const manifest = path.join(store, name, 'oh_modules/@rnoh/react-native-openharmony/oh-package.json5');
      within(root, fs.realpathSync(manifest));
      assert.equal(JSON.parse(fs.readFileSync(manifest, 'utf8')).version, '0.72.143',
        `Stale RNOH store entry: ${manifest}`);
    }
  }

  // Validate every install location before replacing any files.
  const plans = dirs.map(cpp => {
    within(root, cpp);
    const manifest = path.resolve(cpp, '../../../oh-package.json5');
    within(root, fs.realpathSync(manifest));
    assert.equal(JSON.parse(fs.readFileSync(manifest, 'utf8')).version, '0.72.143',
      `RNOH must be 0.72.143: ${manifest}`);
    const target = path.join(cpp, 'third-party/libevent');
    within(root, fs.realpathSync(path.dirname(target)));
    if (fs.existsSync(target)) {
      assert(!fs.lstatSync(target).isSymbolicLink(), `Refusing linked libevent tree: ${target}`);
      within(root, fs.realpathSync(target));
    }
    const cmake = fs.readFileSync(path.join(cpp, 'CMakeLists.txt'), 'utf8');
    const sourceNames = [...cmake.matchAll(/\$\{libevent_src_dir\}\/([^"\s]+\.c)/g)].map(m => m[1]);
    assert(sourceNames.length > 0, 'No libevent compile sources found in RNOH CMake');
    sourceNames.forEach(name => assert(sourceFiles.includes(name), `Missing CMake source: ${name}`));
    const configs = abiNames.map(abi => {
      const file = path.join(cpp, 'patches/libevent', abi, 'event2/event-config.h');
      within(root, fs.realpathSync(file));
      const original = fs.readFileSync(file, 'utf8');
      let updated = original;
      for (const [name, value] of Object.entries(macros)) {
        const pattern = new RegExp(`^#define[ \\t]+${name}[ \\t]+[^\\r\\n]+`, 'gm');
        assert.equal([...updated.matchAll(pattern)].length, 1, `Missing/duplicate ${name}: ${file}`);
        updated = updated.replace(pattern, `#define ${name} ${value}`);
      }
      return {file, original, updated};
    });
    return {cpp, target, configs, compileSources: sourceNames.length};
  });

  for (const {cpp, target, configs, compileSources} of plans) {
    const identical = sameTree(source, target, sourceFiles);
    if (checkOnly) {
      assert(identical && configs.every(c => c.original === c.updated),
        `libevent patch missing or stale in ${cpp}; run this script without --check after ohpm install`);
    } else {
      if (!identical) {
        // The resolved target and its parent were checked against this Harmony project above.
        fs.rmSync(target, {recursive: true, force: true});
        fs.cpSync(source, target, {recursive: true});
      }
      configs.forEach(c => { if (c.original !== c.updated) fs.writeFileSync(c.file, c.updated); });
      assert(sameTree(source, target, sourceFiles), `Source copy verification failed: ${target}`);
      configs.forEach(c => assert.equal(fs.readFileSync(c.file, 'utf8'), c.updated));
    }
    console.log(JSON.stringify({mode: checkOnly ? 'check' : 'patch', cpp,
      rnoh: '0.72.143', libevent: '2.1.13-stable', vendorFiles: sourceFiles.length,
      compileSources, abis: abiNames, macros}));
  }
}

try { main(); } catch (error) { console.error(error.message); process.exitCode = 1; }