const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const { execFileSync } = require('child_process');

const projectRoot = path.join(__dirname, '..');
const configArg = process.argv.find((value) => value.startsWith('--config='));
const configPath = configArg
  ? path.resolve(configArg.slice('--config='.length))
  : path.join(projectRoot, 'config', 'resilienceBackupPlan.example.json');

function sha256File(filePath) {
  const hash = crypto.createHash('sha256');
  const fd = fs.openSync(filePath, 'r');
  const buffer = Buffer.allocUnsafe(1024 * 1024);
  try {
    let bytesRead = 0;
    do {
      bytesRead = fs.readSync(fd, buffer, 0, buffer.length, null);
      if (bytesRead > 0) hash.update(buffer.subarray(0, bytesRead));
    } while (bytesRead > 0);
  } finally {
    fs.closeSync(fd);
  }
  return hash.digest('hex');
}

function commandAvailable(name, binary = null) {
  if (binary?.path) {
    const resolvedPath = path.resolve(binary.path);
    const exists = fs.existsSync(resolvedPath) && fs.statSync(resolvedPath).isFile();
    const expectedSha256 = String(binary.sha256 || '').trim().toLowerCase();
    const actualSha256 = exists ? sha256File(resolvedPath) : '';
    return {
      name,
      available: exists && /^[a-f0-9]{64}$/.test(expectedSha256) && actualSha256 === expectedSha256,
      source: 'explicit-pinned-path',
      path: resolvedPath,
      version: String(binary.version || ''),
      expectedSha256,
      actualSha256
    };
  }
  try {
    const finder = process.platform === 'win32' ? 'where.exe' : 'which';
    execFileSync(finder, [name], { stdio: 'ignore', timeout: 5000, windowsHide: true });
    return { name, available: true, source: 'PATH' };
  } catch (_) {
    return { name, available: false, source: 'PATH' };
  }
}

function directoryStats(root) {
  try {
    if (!fs.existsSync(root)) return { exists: false, accessible: false, files: 0, bytes: 0 };
    const stat = fs.statSync(root);
    if (!stat.isDirectory()) return { exists: true, accessible: true, directory: false, files: 1, bytes: stat.size };
    let files = 0;
    let bytes = 0;
    const stack = [root];
    while (stack.length) {
      const current = stack.pop();
      for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
        const target = path.join(current, entry.name);
        if (entry.isSymbolicLink()) continue;
        if (entry.isDirectory()) stack.push(target);
        if (entry.isFile()) {
          const item = fs.statSync(target);
          files += 1;
          bytes += item.size;
        }
      }
    }
    return { exists: true, accessible: true, directory: true, files, bytes };
  } catch (error) {
    return { exists: true, accessible: false, files: 0, bytes: 0, errorCode: error.code || 'READ_FAILED' };
  }
}

function check(key, ok, detail) {
  return { key, ok: Boolean(ok), detail };
}

function validateNoKeyMaterial(value, location = 'config') {
  if (Array.isArray(value)) {
    value.forEach((item, index) => validateNoKeyMaterial(item, `${location}[${index}]`));
    return;
  }
  if (!value || typeof value !== 'object') return;
  for (const [key, item] of Object.entries(value)) {
    if (/privateKey|passphrase|seed|mnemonic|secretValue/i.test(key) && item) {
      throw new Error(`Key material is forbidden in backup plan: ${location}.${key}`);
    }
    validateNoKeyMaterial(item, `${location}.${key}`);
  }
}

function main() {
  const plan = JSON.parse(fs.readFileSync(configPath, 'utf8'));
  validateNoKeyMaterial(plan);
  const checks = [];
  checks.push(check('schema', plan.schemaVersion === 'plai-resilience-backup-plan/v1', plan.schemaVersion));
  checks.push(check('plan_only_mode', plan.mode === 'PLAN_ONLY_NO_BACKUP_EXECUTION', plan.mode));
  checks.push(check('production_traffic_locked', plan.locks?.productionTrafficChangeAllowed === false, 'must remain false'));
  checks.push(check('database_write_locked', plan.locks?.databaseWriteAllowed === false, 'must remain false'));
  checks.push(check('dns_locked', plan.locks?.dnsChangeAllowed === false, 'must remain false'));
  checks.push(check('external_transfers_locked', plan.locks?.externalTransfersLocked === true, 'must remain true'));
  checks.push(check('operator_signing_locked', plan.locks?.operatorSigningLocked === true, 'must remain true'));
  checks.push(check('validator_action_locked', plan.locks?.validatorActionAllowed === false, 'must remain false'));

  const tools = (plan.requiredTools || []).map((name) => commandAvailable(name, plan.toolBinaries?.[name]));
  checks.push(check('required_tools', tools.every((tool) => tool.available), tools.map((tool) => `${tool.name}:${tool.available}`).join(', ')));

  const datasets = (plan.fileDatasets || []).map((dataset) => ({
    id: dataset.id,
    classification: dataset.classification,
    required: dataset.required === true,
    path: dataset.path,
    ...directoryStats(dataset.path)
  }));
  checks.push(check('required_sources_exist', datasets.filter((row) => row.required).every((row) => row.exists && row.accessible), `${datasets.filter((row) => row.exists && row.accessible).length}/${datasets.length}`));
  const exclusions = Array.isArray(plan.explicitExclusions) ? plan.explicitExclusions : [];
  checks.push(check('sensitive_paths_explicitly_excluded', [
    'operator-custody',
    '*.env',
    'validator-*',
    'private keys',
    'approval files'
  ].every((needle) => exclusions.some((row) => String(row).toLowerCase().includes(needle.toLowerCase()))), `${exclusions.length} exclusions`));

  const destination = plan.destination || {};
  checks.push(check('independent_destination_named', Boolean(destination.recoveryNode && destination.rootPath), 'recoveryNode and rootPath must be set'));
  checks.push(check('same_host_destination_forbidden', destination.sameHostAsPrimaryAllowed === false, 'must remain false'));
  checks.push(check('encryption_recipient_named', Boolean(plan.encryption?.recipient), 'offline public recipient is required'));
  checks.push(check('private_key_forbidden_on_primary', plan.encryption?.privateKeyOnPrimaryAllowed === false, 'must remain false'));
  checks.push(check('restore_host_named', Boolean(plan.isolatedRestore?.host), 'isolated restore host is required'));
  checks.push(check('restore_network_isolated', plan.isolatedRestore?.networkMode === 'NO_PRODUCTION_ROUTE', plan.isolatedRestore?.networkMode));
  checks.push(check('restore_workers_fenced', plan.isolatedRestore?.writeWorkersEnabled === false, 'must remain false'));
  checks.push(check('restore_validator_fenced', plan.isolatedRestore?.validatorEnabled === false, 'must remain false'));

  const failed = checks.filter((row) => !row.ok);
  console.log(JSON.stringify({
    ok: true,
    mode: 'READ_ONLY_PREFLIGHT_NO_BACKUP_EXECUTION',
    configPath,
    readyForPhase1BackupExecution: failed.length === 0,
    status: failed.length === 0 ? 'READY_FOR_SEPARATE_EXECUTION_APPROVAL' : 'SAFE_BLOCKED_MISSING_PREREQUISITES',
    checks,
    tools,
    datasets,
    totals: {
      files: datasets.reduce((sum, row) => sum + Number(row.files || 0), 0),
      bytes: datasets.reduce((sum, row) => sum + Number(row.bytes || 0), 0)
    },
    safety: {
      environmentRead: false,
      databaseConnected: false,
      networkRequestsMade: false,
      filesWritten: false,
      backupExecuted: false,
      restoreExecuted: false,
      failedClosed: failed.length > 0
    },
    failedChecks: failed.map((row) => row.key),
    checkedAt: new Date().toISOString()
  }, null, 2));
}

try {
  main();
} catch (error) {
  console.error(JSON.stringify({ ok: false, mode: 'READ_ONLY_PREFLIGHT_NO_BACKUP_EXECUTION', error: error.message }, null, 2));
  process.exitCode = 1;
}
