require('../config/loadEnv')();
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
const { Wallet, getAddress } = require('ethers');
const { auditAcl, protect, restrictAcl, unprotect } = require('../lib/operatorDpapi');
const operatorCustodyService = require('../services/operatorCustodyService');

const mode = String(process.argv[2] || '').trim().toLowerCase();
const projectRoot = path.join(__dirname, '..');
const networkPath = path.join(projectRoot, 'runtime', 'plais-qbft', 'network.json');
const transferSchema = 'plais-operator-vault-runtime-rewrap/v1';
const transferLifetimeMs = 60 * 60 * 1000;

function fail(code, message) {
  throw Object.assign(new Error(message), { code });
}

function readJson(filePath) {
  return JSON.parse(fs.readFileSync(filePath, 'utf8'));
}

function sha256File(filePath) {
  return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
}

function getPaths() {
  const vaultPath = operatorCustodyService.resolveVaultPath();
  return {
    vaultPath,
    transferPath: `${vaultPath}.runtime-rewrap.dpapi-machine.json`,
    backupPath: `${vaultPath}.pre-runtime-rewrap`
  };
}

function runDpapiTransform(sourceCiphertext, sourceScope, targetScope) {
  if (!['CurrentUser', 'LocalMachine'].includes(sourceScope) || !['CurrentUser', 'LocalMachine'].includes(targetScope)) {
    fail('DPAPI_SCOPE_INVALID', 'Unsupported DPAPI rewrap scope.');
  }
  const script = [
    "$ErrorActionPreference = 'Stop'",
    'Add-Type -AssemblyName System.Security',
    '$source = $null',
    '$plain = $null',
    '$target = $null',
    '$verify = $null',
    'try {',
    '  $value = [Console]::In.ReadToEnd().Trim()',
    '  $source = [Convert]::FromBase64String($value)',
    "  $entropy = [Text.Encoding]::UTF8.GetBytes('PLAIS_OPERATOR_CUSTODY_V1')",
    `  $plain = [Security.Cryptography.ProtectedData]::Unprotect($source, $entropy, [Security.Cryptography.DataProtectionScope]::${sourceScope})`,
    `  $target = [Security.Cryptography.ProtectedData]::Protect($plain, $entropy, [Security.Cryptography.DataProtectionScope]::${targetScope})`,
    `  $verify = [Security.Cryptography.ProtectedData]::Unprotect($target, $entropy, [Security.Cryptography.DataProtectionScope]::${targetScope})`,
    '  if ($verify.Length -ne $plain.Length) { throw "DPAPI rewrap length verification failed." }',
    '  for ($index = 0; $index -lt $plain.Length; $index += 1) { if ($verify[$index] -ne $plain[$index]) { throw "DPAPI rewrap content verification failed." } }',
    '  [Convert]::ToBase64String($target)',
    '} finally {',
    '  foreach ($buffer in @($source, $plain, $target, $verify, $entropy)) { if ($null -ne $buffer) { [Array]::Clear($buffer, 0, $buffer.Length) } }',
    '}'
  ].join('\n');
  const result = spawnSync('powershell.exe', [
    '-NoLogo',
    '-NoProfile',
    '-NonInteractive',
    '-ExecutionPolicy', 'Bypass',
    '-Command', script
  ], {
    input: String(sourceCiphertext || ''),
    encoding: 'utf8',
    windowsHide: true,
    maxBuffer: 1024 * 1024
  });
  const output = String(result.stdout || '').trim();
  if (result.status !== 0 || !/^[A-Za-z0-9+/=]{32,}$/.test(output)) {
    const diagnostic = String(result.stderr || '').replace(/[\r\n]+/g, ' ').trim().slice(0, 500);
    fail('DPAPI_REWRAP_FAILED', `DPAPI rewrap failed with exit code ${result.status}${diagnostic ? `: ${diagnostic}` : '.'}`);
  }
  return output;
}

function getCanonicalIdentity() {
  const configuredAddress = getAddress(String(process.env.PLAIS_EVM_OPERATOR_ADDRESS || '').trim());
  const network = readJson(networkPath);
  const networkAddress = getAddress(network.operatorAddress);
  if (configuredAddress !== networkAddress) fail('OPERATOR_ADDRESS_MISMATCH', 'Configured and canonical operator addresses differ.');
  const genesis = readJson(path.resolve(network.genesisFile));
  if (!genesis.alloc || !genesis.alloc[configuredAddress.slice(2).toLowerCase()]) {
    fail('OPERATOR_GENESIS_ALLOCATION_MISSING', 'Canonical operator allocation is missing from genesis.');
  }
  return configuredAddress;
}

function validateVaultIdentity(vaultPath) {
  const canonicalAddress = getCanonicalIdentity();
  const metadata = readJson(vaultPath);
  if (metadata.version !== 1 || metadata.protection !== operatorCustodyService.EXPECTED_PROTECTION) {
    fail('OPERATOR_VAULT_FORMAT_INVALID', 'Operator vault format or protection label is invalid.');
  }
  if (getAddress(metadata.operatorAddress) !== canonicalAddress) {
    fail('OPERATOR_VAULT_ADDRESS_MISMATCH', 'Operator vault metadata address is not canonical.');
  }
  const recoveredKey = unprotect(metadata.ciphertext);
  try {
    if (new Wallet(recoveredKey).address !== canonicalAddress) {
      fail('OPERATOR_VAULT_KEY_MISMATCH', 'Recovered operator key is not canonical.');
    }
  } finally {
    // JavaScript strings cannot be zeroed; this process exits immediately after the ceremony step.
  }
  return { metadata, canonicalAddress };
}

function writeJsonRestricted(filePath, value) {
  const tempPath = `${filePath}.tmp-${process.pid}-${crypto.randomBytes(4).toString('hex')}`;
  fs.writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
  restrictAcl(tempPath, false);
  fs.renameSync(tempPath, filePath);
  const acl = auditAcl(filePath);
  if (!acl.ok) fail('REWRAP_FILE_ACL_FAILED', 'Rewrap file ACL verification failed.');
}

function stage() {
  const { vaultPath, transferPath, backupPath } = getPaths();
  if (fs.existsSync(transferPath) || fs.existsSync(backupPath)) {
    fail('REWRAP_ARTIFACT_EXISTS', 'A previous runtime rewrap artifact already exists.');
  }
  const { metadata, canonicalAddress } = validateVaultIdentity(vaultPath);
  const sourceVaultSha256 = sha256File(vaultPath);
  const machineCiphertext = runDpapiTransform(metadata.ciphertext, 'CurrentUser', 'LocalMachine');
  const issuedAt = new Date();
  writeJsonRestricted(transferPath, {
    schemaVersion: transferSchema,
    sourceVaultSha256,
    sourceProtection: operatorCustodyService.EXPECTED_PROTECTION,
    targetProtection: operatorCustodyService.EXPECTED_PROTECTION,
    operatorAddress: canonicalAddress,
    machineCiphertext,
    issuedAt: issuedAt.toISOString(),
    expiresAt: new Date(issuedAt.getTime() + transferLifetimeMs).toISOString()
  });
  console.log(JSON.stringify({
    ok: true,
    mode: 'stage',
    plaintextWritten: false,
    sourceIdentityVerified: true,
    encryptedTransferWritten: true,
    expiresInMinutes: transferLifetimeMs / 60000
  }));
}

function validateTransfer(transfer, vaultPath) {
  if (transfer.schemaVersion !== transferSchema) fail('REWRAP_SCHEMA_INVALID', 'Runtime rewrap transfer schema is invalid.');
  const issuedAt = new Date(transfer.issuedAt).getTime();
  const expiresAt = new Date(transfer.expiresAt).getTime();
  const now = Date.now();
  if (!Number.isFinite(issuedAt) || !Number.isFinite(expiresAt) || expiresAt <= issuedAt || expiresAt < now || expiresAt - issuedAt > transferLifetimeMs) {
    fail('REWRAP_TRANSFER_EXPIRED', 'Runtime rewrap transfer is expired or malformed.');
  }
  if (transfer.sourceVaultSha256 !== sha256File(vaultPath)) fail('REWRAP_SOURCE_CHANGED', 'Operator vault changed after staging.');
  if (getAddress(transfer.operatorAddress) !== getCanonicalIdentity()) fail('REWRAP_ADDRESS_MISMATCH', 'Runtime rewrap transfer address is not canonical.');
  if (transfer.sourceProtection !== operatorCustodyService.EXPECTED_PROTECTION || transfer.targetProtection !== operatorCustodyService.EXPECTED_PROTECTION) {
    fail('REWRAP_PROTECTION_INVALID', 'Runtime rewrap protection binding is invalid.');
  }
}

function finalize() {
  const { vaultPath, transferPath, backupPath } = getPaths();
  if (!fs.existsSync(transferPath)) fail('REWRAP_TRANSFER_MISSING', 'Encrypted runtime rewrap transfer is missing.');
  if (fs.existsSync(backupPath)) fail('REWRAP_BACKUP_EXISTS', 'Runtime rewrap backup already exists.');
  const transfer = readJson(transferPath);
  validateTransfer(transfer, vaultPath);
  const sourceMetadata = readJson(vaultPath);
  const runtimeCiphertext = runDpapiTransform(transfer.machineCiphertext, 'LocalMachine', 'CurrentUser');
  const runtimeMetadata = {
    ...sourceMetadata,
    version: 1,
    protection: operatorCustodyService.EXPECTED_PROTECTION,
    operatorAddress: getCanonicalIdentity(),
    ciphertext: runtimeCiphertext,
    aclProtected: true,
    rewrappedAt: new Date().toISOString()
  };
  const tempPath = `${vaultPath}.runtime-${process.pid}-${crypto.randomBytes(4).toString('hex')}.tmp`;
  writeJsonRestricted(tempPath, runtimeMetadata);
  fs.renameSync(vaultPath, backupPath);
  try {
    fs.renameSync(tempPath, vaultPath);
    restrictAcl(path.dirname(vaultPath), true);
    restrictAcl(vaultPath, false);
    restrictAcl(backupPath, false);
    validateVaultIdentity(vaultPath);
  } catch (error) {
    if (fs.existsSync(vaultPath)) fs.unlinkSync(vaultPath);
    if (fs.existsSync(backupPath)) fs.renameSync(backupPath, vaultPath);
    if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
    throw error;
  }
  console.log(JSON.stringify({
    ok: true,
    mode: 'finalize',
    plaintextWritten: false,
    runtimeIdentityVerified: true,
    canonicalAddressVerified: true,
    backupRetainedUntilAudit: true
  }));
}

function cleanup() {
  const { vaultPath, transferPath, backupPath } = getPaths();
  const { metadata } = validateVaultIdentity(vaultPath);
  if (!metadata.rewrappedAt) fail('REWRAP_NOT_FINALIZED', 'Runtime rewrap metadata is missing.');
  const vaultAcl = auditAcl(vaultPath);
  const directoryAcl = auditAcl(path.dirname(vaultPath));
  if (!vaultAcl.ok || !directoryAcl.ok) fail('REWRAP_ACL_INVALID', 'Runtime vault ACL audit failed.');
  if (!fs.existsSync(transferPath) || !fs.existsSync(backupPath)) fail('REWRAP_CLEANUP_ARTIFACT_MISSING', 'Expected rewrap artifacts are missing.');
  const transfer = readJson(transferPath);
  if (sha256File(backupPath) !== transfer.sourceVaultSha256) fail('REWRAP_BACKUP_MISMATCH', 'Runtime rewrap backup integrity check failed.');
  fs.unlinkSync(transferPath);
  fs.unlinkSync(backupPath);
  console.log(JSON.stringify({
    ok: true,
    mode: 'cleanup',
    runtimeIdentityVerified: true,
    encryptedTransferRemoved: true,
    oldProfileVaultRemoved: true,
    plaintextWritten: false
  }));
}

function selfTest() {
  const sample = `operator-rewrap-self-test-${crypto.randomBytes(24).toString('hex')}`;
  const source = protect(sample);
  const machine = runDpapiTransform(source, 'CurrentUser', 'LocalMachine');
  const target = runDpapiTransform(machine, 'LocalMachine', 'CurrentUser');
  if (unprotect(target) !== sample) fail('REWRAP_SELF_TEST_FAILED', 'DPAPI rewrap self-test failed.');
  console.log(JSON.stringify({
    ok: true,
    currentUserToLocalMachine: 'pass',
    localMachineToCurrentUser: 'pass',
    roundTrip: 'pass',
    plaintextWritten: false
  }));
}

try {
  if (mode === 'stage') stage();
  else if (mode === 'finalize') finalize();
  else if (mode === 'cleanup') cleanup();
  else if (mode === 'self-test') selfTest();
  else fail('REWRAP_MODE_REQUIRED', 'Use stage, finalize, cleanup, or self-test.');
} catch (error) {
  console.error(JSON.stringify({ ok: false, error: error.code || 'OPERATOR_RUNTIME_REWRAP_FAILED', message: error.message }));
  process.exit(1);
}
