require('../config/loadEnv')();

const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const persistentStore = require('../services/persistentStore');
const { matchesValidatorTarget } = require('../services/validatorTargetBindingService');
const { ENVELOPE_SCHEMA, KEY_ALGORITHM } = require('../lib/validatorKeyEnvelope');
const { auditAcl } = require('../lib/operatorDpapi');

const projectRoot = path.join(__dirname, '..');
const activeRoot = path.join(projectRoot, 'runtime', 'plais-qbft');
const migrationRoot = path.join(projectRoot, 'runtime', 'plais-qbft-canonical-migration');
const topologyPath = path.join(migrationRoot, 'topology.json');
const approvalPath = path.join(migrationRoot, 'encrypted-key-export-approval.json');
const outputRoot = path.join(migrationRoot, 'encrypted-key-envelopes');

function fail(message) {
  throw new Error(message);
}

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

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

function normalizePublicKey(value) {
  return String(value || '').trim().toLowerCase().replace(/^0x/, '').replace(/^04/, '');
}

function latestNodeForTarget(nodes, target) {
  return nodes
    .filter((node) => matchesValidatorTarget(node, target))
    .sort((left, right) => new Date(right.lastHeartbeatAt || 0) - new Date(left.lastHeartbeatAt || 0))[0] || null;
}

function hasForbiddenPlaintextField(value) {
  if (Array.isArray(value)) return value.some(hasForbiddenPlaintextField);
  if (!value || typeof value !== 'object') return false;
  const forbidden = new Set(['privatekey', 'validatorprivatekey', 'plaintext', 'secret']);
  return Object.entries(value).some(([key, child]) => forbidden.has(key.toLowerCase()) || hasForbiddenPlaintextField(child));
}

function main() {
  if (!fs.existsSync(outputRoot)) fail('Encrypted validator envelope directory is missing.');
  if (fs.existsSync(approvalPath)) fail('One-time key encryption approval was not consumed.');
  if (!auditAcl(outputRoot).ok) fail('Encrypted validator envelope directory ACL is not protected.');
  const topology = readJson(topologyPath);
  const network = readJson(path.join(activeRoot, 'network.json'));
  const manifestPath = path.join(outputRoot, 'manifest.json');
  const manifest = readJson(manifestPath);
  if (!auditAcl(manifestPath).ok) fail('Encrypted validator manifest ACL is not protected.');
  if (
    manifest.schemaVersion !== 'plais-encrypted-validator-key-delivery/v1'
    || manifest.plaintextKeyFilesWritten !== false
    || manifest.aclProtected !== true
    || Number(manifest.chainId) !== Number(topology.chainId)
    || manifest.genesisSha256 !== topology.genesisSha256
    || !Array.isArray(manifest.nodes)
    || manifest.nodes.length !== 4
  ) {
    fail('Encrypted validator manifest binding is invalid.');
  }
  const expectedNames = new Set(['manifest.json']);
  const stateNodes = persistentStore.readState().nodes || [];
  for (const target of topology.validators || []) {
    const row = manifest.nodes.find((entry) => entry.nodeName === target.nodeName && entry.pcName === target.pcName);
    if (!row) fail(`Encrypted validator manifest target is missing: ${target.nodeName}`);
    const activeNode = (network.nodes || []).find((node) => node.name === target.nodeName && node.role === 'validator');
    const participant = latestNodeForTarget(stateNodes, target);
    if (!activeNode || !participant) fail(`Validator identity source is missing: ${target.nodeName}`);
    if (row.deviceKeyId !== participant.metadata?.provisioningKeyId) fail(`Device key binding changed: ${target.nodeName}`);
    const envelopePath = path.join(outputRoot, row.fileName);
    expectedNames.add(row.fileName);
    if (!fs.existsSync(envelopePath) || sha256File(envelopePath) !== row.sha256) fail(`Envelope hash mismatch: ${target.nodeName}`);
    if (!auditAcl(envelopePath).ok) fail(`Envelope ACL is not protected: ${target.nodeName}`);
    const envelope = readJson(envelopePath);
    const ciphertext = Buffer.from(String(envelope.ciphertext || ''), 'base64');
    const wrappedContentKey = Buffer.from(String(envelope.wrappedContentKey || ''), 'base64');
    if (
      envelope.schemaVersion !== ENVELOPE_SCHEMA
      || envelope.encryption !== 'aes-256-gcm+rsa-oaep-sha256'
      || envelope.deviceKeyAlgorithm !== KEY_ALGORITHM
      || envelope.deviceKeyId !== row.deviceKeyId
      || envelope.context?.schemaVersion !== ENVELOPE_SCHEMA
      || envelope.context?.deviceKeyId !== row.deviceKeyId
      || Number(envelope.context?.chainId) !== Number(topology.chainId)
      || envelope.context?.genesisSha256 !== topology.genesisSha256
      || envelope.context?.nodeName !== target.nodeName
      || envelope.context?.pcName !== target.pcName
      || String(envelope.context?.validatorAddress || '').toLowerCase() !== String(target.validatorAddress || '').toLowerCase()
      || normalizePublicKey(envelope.context?.validatorPublicKey) !== normalizePublicKey(target.publicKey)
      || envelope.context?.purpose !== 'canonical-validator-host-migration'
      || Buffer.from(String(envelope.iv || ''), 'base64').length !== 12
      || Buffer.from(String(envelope.authTag || ''), 'base64').length !== 16
      || ciphertext.length !== 64
      || wrappedContentKey.length < 384
      || crypto.createHash('sha256').update(ciphertext).digest('hex') !== envelope.ciphertextSha256
      || hasForbiddenPlaintextField(envelope)
    ) {
      fail(`Encrypted validator envelope validation failed: ${target.nodeName}`);
    }
  }
  const actualNames = fs.readdirSync(outputRoot).sort();
  if (actualNames.length !== expectedNames.size || actualNames.some((name) => !expectedNames.has(name))) {
    fail('Unexpected file exists in encrypted validator envelope directory.');
  }
  console.log(JSON.stringify({
    ok: true,
    state: 'ENCRYPTED_KEY_ENVELOPES_READY_NOT_DELIVERED',
    validatorCount: 4,
    manifestHashVerified: true,
    deviceBindingsVerified: true,
    canonicalContextVerified: true,
    ciphertextIntegrityVerified: true,
    plaintextFieldsAbsent: true,
    aclProtected: true,
    approvalConsumed: true,
    delivered: false,
    imported: false,
    validatorsStarted: false
  }, null, 2));
}

try {
  main();
} catch (error) {
  console.error(JSON.stringify({ ok: false, error: error.message }, null, 2));
  process.exit(1);
}
