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

const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const { computeAddress, SigningKey } = require('ethers');
const persistentStore = require('../services/persistentStore');
const { matchesValidatorTarget } = require('../services/validatorTargetBindingService');
const { encryptValidatorKeyEnvelope, parseDevicePublicKey, stableValue } = require('../lib/validatorKeyEnvelope');
const { auditAcl, restrictAcl } = 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');
const supportedClientVersions = new Set([
  '1.4.30-validator-tunnel-approval-retry-single-install-locked',
  '1.4.31-validator-envelope-identity-rotation-single-install-locked',
  '1.4.32-validator-bridge-fallback-single-install-locked',
  '1.4.33-validator-presync-bridge-recovery-single-install-locked',
  '1.4.34-validator-runtime-diagnostic-single-install-locked',
  '1.4.35-validator-runtime-worker-guard-single-install-locked',
  '1.4.36-validator-memory-safe-single-install-locked',
  '1.4.37-validator-rpc-ready-recovery-single-install-locked',
  '1.4.38-validator-startup-monitor-single-install-locked',
  '1.4.39-validator-error-retention-single-install-locked',
  '1.4.40-validator-exit-code-diagnostic-single-install-locked',
  '1.4.41-validator-complete-exit-diagnostic-single-install-locked',
  '1.4.42-validator-dll-basename-diagnostic-single-install-locked',
  '1.4.43-validator-app-local-vc-runtime-single-install-locked',
  '1.4.44-validator-snap-presync-single-install-locked',
  '1.4.45-validator-activation-diagnostic-single-install-locked',
  '1.4.46-validator-failure-retention-single-install-locked',
  '1.4.47-validator-worker-lock-identity-single-install-locked',
  '1.4.48-validator-runtime-pid-identity-single-install-locked',
  '1.4.49-validator-tunnel-state-preservation-single-install-locked',
  '1.4.50-validator-worker-race-single-install-locked',
  '1.4.51-validator-orphan-worker-cleanup-single-install-locked',
  '1.4.52-validator-existing-besu-adoption-single-install-locked',
  '1.4.53-validator-static-topology-reconcile-single-install-locked',
  '1.4.54-validator-preserved-full-bridge-recovery-single-install-locked',
  '1.4.55-validator-tunnel-approval-refresh-single-install-locked',
  '1.4.56-validator-directed-tunnel-topology-single-install-locked',
  '1.4.57-validator-loopback-tunnel-peer-diagnostic-single-install-locked',
  '1.4.58-validator-launch-config-reconcile-single-install-locked',
  '1.4.59-validator-p2p-disconnect-diagnostic-single-install-locked',
  '1.4.60-validator-unique-advertised-endpoint-single-install-locked',
  '1.4.61-validator-active-data-path-reconcile-single-install-locked',
  '1.4.62-validator-approved-consensus-restart-single-install-locked',
  '1.4.63-validator-qbft-consensus-diagnostic-single-install-locked',
  '1.4.107-quick-tunnel-discovery-default-off-locked',
  '1.4.109-runtime-heartbeat-null-stale-fallback-default-off-locked',
  '1.4.110-data-replica-readiness-attestation-default-off-locked'
]);

function sha256Buffer(value) {
  return crypto.createHash('sha256').update(value).digest('hex');
}

function sha256File(filePath) {
  return sha256Buffer(fs.readFileSync(filePath));
}

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 normalizePublicKey(value) {
  return String(value || '').trim().toLowerCase().replace(/^0x/, '').replace(/^04/, '');
}

function getReadyTargets(topology) {
  const nodes = persistentStore.readState().nodes || [];
  const now = Date.now();
  const rows = (topology.validators || []).map((target) => {
    const node = latestNodeForTarget(nodes, target);
    const metadata = node?.metadata || {};
    const heartbeatAgeSec = node?.lastHeartbeatAt
      ? Math.max(0, Math.round((now - new Date(node.lastHeartbeatAt).getTime()) / 1000))
      : null;
    const checks = {
      registered: Boolean(node),
      active: node?.status === 'active' && Number.isFinite(heartbeatAgeSec) && heartbeatAgeSec <= 300,
      clientReady: supportedClientVersions.has(String(node?.clientVersion || '')),
      receiptReady: metadata.chainProvisioningSchema === 'plais-node-provisioning/v1'
        && /^[a-f0-9]{64}$/.test(String(metadata.chainProvisioningHash || ''))
        && metadata.chainProvisioningRole === 'validator-candidate'
        && metadata.chainProvisioningState === 'awaiting-operator-approval',
      deviceKeyReady: metadata.provisioningKeyAlgorithm === 'rsa-oaep-sha256-dpapi-current-user-v1'
        && /^[a-f0-9]{64}$/.test(String(metadata.provisioningKeyId || ''))
        && Boolean(metadata.provisioningPublicKey)
    };
    if (checks.deviceKeyReady) {
      try {
        parseDevicePublicKey(metadata.provisioningPublicKey, metadata.provisioningKeyId);
      } catch (error) {
        checks.deviceKeyReady = false;
      }
    }
    return {
      target,
      node,
      metadata,
      checks,
      ready: Object.values(checks).every(Boolean)
    };
  });
  return rows;
}

function validateApproval(topology, rows, genesisSha256) {
  if (!fs.existsSync(approvalPath)) return { ok: false, reason: 'KEY_ENCRYPTION_APPROVAL_MISSING' };
  const approval = JSON.parse(fs.readFileSync(approvalPath, 'utf8'));
  const expectedKeyIds = Object.fromEntries(rows.map((row) => [row.target.nodeName, row.metadata.provisioningKeyId]));
  const expiresAt = new Date(approval.expiresAt || 0).getTime();
  const approvedAt = new Date(approval.approvedAt || 0).getTime();
  const valid = approval.action === 'ENCRYPT_CANONICAL_VALIDATOR_KEYS'
    && approval.topologySha256 === sha256File(topologyPath)
    && approval.genesisSha256 === genesisSha256
    && JSON.stringify(stableValue(approval.deviceKeyIds || {})) === JSON.stringify(stableValue(expectedKeyIds))
    && /^[A-Za-z0-9._:-]{8,128}$/.test(String(approval.approvalId || ''))
    && Boolean(String(approval.approvedBy || '').trim())
    && Number.isFinite(approvedAt)
    && approvedAt <= Date.now()
    && approvedAt >= Date.now() - 24 * 60 * 60 * 1000
    && Number.isFinite(expiresAt)
    && expiresAt > Date.now()
    && expiresAt <= Date.now() + 24 * 60 * 60 * 1000;
  return valid ? { ok: true, approval } : { ok: false, reason: 'KEY_ENCRYPTION_APPROVAL_INVALID' };
}

function validateCanonicalPrivateKey(activeNode, target) {
  const keyPath = path.join(activeRoot, activeNode.name, 'data', 'key');
  const privateKey = fs.readFileSync(keyPath, 'utf8').trim();
  const signingKey = new SigningKey(privateKey.startsWith('0x') ? privateKey : `0x${privateKey}`);
  const publicKey = normalizePublicKey(signingKey.publicKey);
  const address = computeAddress(signingKey.publicKey).toLowerCase();
  if (
    publicKey !== normalizePublicKey(target.publicKey) ||
    address !== String(target.validatorAddress || '').toLowerCase() ||
    address !== String(activeNode.address || '').toLowerCase()
  ) {
    throw new Error(`Canonical validator identity mismatch for ${activeNode.name}.`);
  }
  return { privateKey, address, publicKey };
}

function writeJson(filePath, value) {
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
  fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
  const acl = restrictAcl(filePath, false);
  if (!acl.ok) throw new Error(`Encrypted validator envelope ACL verification failed: ${path.basename(filePath)}`);
}

function main() {
  if (!fs.existsSync(topologyPath)) throw new Error('Canonical migration topology is missing.');
  if (fs.existsSync(outputRoot)) throw new Error('Encrypted validator key envelopes already exist; refusing to overwrite them.');
  const topology = JSON.parse(fs.readFileSync(topologyPath, 'utf8'));
  if (topology.mode !== 'preserve-canonical-genesis' || !Array.isArray(topology.validators) || topology.validators.length !== 4) {
    throw new Error('Canonical four-validator migration topology is invalid.');
  }

  const rows = getReadyTargets(topology);
  if (!rows.every((row) => row.ready)) {
    console.log(JSON.stringify({
      ok: false,
      blocked: true,
      reason: 'SYNC_CLIENT_ENCRYPTION_ENROLLMENT_INCOMPLETE',
      liveValidatorKeysRead: false,
      outputWritten: false,
      rows: rows.map((row) => ({ pcName: row.target.pcName, nodeName: row.target.nodeName, checks: row.checks }))
    }, null, 2));
    process.exitCode = 2;
    return;
  }

  const networkPath = path.join(activeRoot, 'network.json');
  const genesisPath = path.join(activeRoot, 'generated', 'genesis.json');
  const network = JSON.parse(fs.readFileSync(networkPath, 'utf8'));
  const genesisSha256 = sha256File(genesisPath);
  if (Number(network.chainId) !== Number(topology.chainId) || genesisSha256 !== topology.genesisSha256) {
    throw new Error('Canonical chain ID or genesis hash changed after migration initialization.');
  }
  const approvalResult = validateApproval(topology, rows, genesisSha256);
  if (!approvalResult.ok) {
    console.log(JSON.stringify({
      ok: false,
      blocked: true,
      reason: approvalResult.reason,
      liveValidatorKeysRead: false,
      outputWritten: false,
      approvalPath
    }, null, 2));
    process.exitCode = 3;
    return;
  }

  const validators = (network.nodes || []).filter((node) => node.role === 'validator');
  if (validators.length !== 4) throw new Error('Active canonical validator set is not four nodes.');
  const tempRoot = `${outputRoot}.tmp-${process.pid}-${Date.now()}`;
  fs.mkdirSync(tempRoot, { recursive: true });
  const tempRootAcl = restrictAcl(tempRoot, true);
  if (!tempRootAcl.ok) throw new Error('Encrypted validator envelope directory ACL verification failed.');
  try {
    const manifestRows = [];
    for (const row of rows) {
      const activeNode = validators.find((node) => node.name === row.target.nodeName);
      if (!activeNode) throw new Error(`Active validator not found: ${row.target.nodeName}`);
      const identity = validateCanonicalPrivateKey(activeNode, row.target);
      const envelope = encryptValidatorKeyEnvelope({
        validatorPrivateKey: identity.privateKey,
        publicKeyBase64: row.metadata.provisioningPublicKey,
        deviceKeyId: row.metadata.provisioningKeyId,
        context: {
          chainId: Number(network.chainId),
          genesisSha256,
          nodeName: activeNode.name,
          pcName: row.target.pcName,
          validatorAddress: identity.address,
          validatorPublicKey: identity.publicKey,
          purpose: 'canonical-validator-host-migration'
        }
      });
      const fileName = `${activeNode.name}.validator-key.envelope.json`;
      const filePath = path.join(tempRoot, fileName);
      writeJson(filePath, envelope);
      manifestRows.push({
        nodeName: activeNode.name,
        pcName: row.target.pcName,
        deviceKeyId: row.metadata.provisioningKeyId,
        validatorAddress: identity.address,
        fileName,
        sha256: sha256File(filePath)
      });
    }
    writeJson(path.join(tempRoot, 'manifest.json'), {
      schemaVersion: 'plais-encrypted-validator-key-delivery/v1',
      chainId: Number(network.chainId),
      genesisSha256,
      topologySha256: sha256File(topologyPath),
      approvalId: String(approvalResult.approval.approvalId || ''),
      plaintextKeyFilesWritten: false,
      aclProtected: true,
      nodes: manifestRows,
      createdAt: new Date().toISOString()
    });
    fs.renameSync(tempRoot, outputRoot);
    const outputAcl = auditAcl(outputRoot);
    if (!outputAcl.ok) throw new Error('Final encrypted validator envelope directory ACL verification failed.');
    fs.unlinkSync(approvalPath);
  } catch (error) {
    if (fs.existsSync(tempRoot)) fs.rmSync(tempRoot, { recursive: true, force: true });
    throw error;
  }
  console.log(JSON.stringify({
    ok: true,
    state: 'ENCRYPTED_KEY_ENVELOPES_READY_NOT_DELIVERED',
    validatorCount: 4,
    plaintextKeyFilesWritten: false,
    aclProtected: true,
    approvalConsumed: true,
    delivered: false,
    imported: false,
    validatorsStarted: false,
    outputRoot
  }, null, 2));
}

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