require('../config/loadEnv')();
const crypto = require('crypto');
const fs = require('fs');
const http = require('http');
const path = require('path');
const persistentStore = require('../services/persistentStore');
const { matchesValidatorTarget } = require('../services/validatorTargetBindingService');
const { restrictAcl, auditAcl } = require('../lib/operatorDpapi');

const root = path.join(__dirname, '..');
const migrationRoot = path.join(root, 'runtime', 'plais-qbft-canonical-migration');
const topologyPath = path.join(migrationRoot, 'topology.json');
const runtimeManifestPath = path.join(root, 'runtime', 'plais-validator-runtime', 'manifest.json');
const keyAttestationPath = path.join(root, 'runtime', 'mainnet-key-security-attestation.json');
const approvalPath = path.join(migrationRoot, 'validator-activation-approval.json');
const pm2StateAttestationPath = path.join(migrationRoot, 'validator-pm2-state-attestation.json');
const confirmPhrase = 'ACTIVATE_REMOTE_VALIDATOR_AFTER_CANONICAL_STOP';
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',
  '1.4.111-validator-node-mode-recovery-single-install-locked'
  ,'1.4.112-validator-control-error-attestation-single-install-locked'
  ,'1.4.113-validator-control-evidence-priority-single-install-locked'
  ,'1.4.114-validator-runtime-evidence-priority-single-install-locked'
  ,'1.4.115-validator-active-tunnel-restart-stability-single-install-locked'
  ,'1.4.116-validator-active-tunnel-restart-stability-single-install-locked'
  ,'1.4.117-validator-direct-lan-quorum-tunnel-fallback-locked'
  ,'1.4.118-validator-direct-lan-runtime-attestation-tunnel-fallback-locked'
  ,'1.4.119-validator-direct-nodeinfo-attestation-tunnel-fallback-locked'
  ,'1.4.120-validator-direct-attestation-refresh-tunnel-fallback-locked'
  ,'1.4.121-validator-direct-evidence-diagnostic-tunnel-fallback-locked'
  ,'1.4.122-validator-direct-offer-state-diagnostic-tunnel-fallback-locked'
  ,'1.4.123-validator-direct-control-heartbeat-diagnostic-tunnel-fallback-locked'
]);

const sha256File = filePath => crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

function args() {
  return Object.fromEntries(process.argv.slice(2).map(value => {
    const match = String(value).match(/^--([a-z-]+)=(.+)$/i);
    return match ? [match[1], match[2]] : ['', ''];
  }).filter(row => row[0]));
}

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

function rpc(method) {
  return new Promise((resolve, reject) => {
    const body = Buffer.from(JSON.stringify({ jsonrpc: '2.0', id: 1, method, params: [] }));
    const request = http.request({ host: '127.0.0.1', port: 8545, method: 'POST', path: '/', headers: { 'Content-Type': 'application/json', 'Content-Length': body.length }, timeout: 5000 }, response => {
      const chunks = [];
      response.on('data', chunk => chunks.push(chunk));
      response.on('end', () => {
        try {
          const parsed = JSON.parse(Buffer.concat(chunks).toString('utf8'));
          if (parsed.error) reject(new Error(parsed.error.message)); else resolve(parsed.result);
        } catch (error) { reject(error); }
      });
    });
    request.on('error', reject);
    request.on('timeout', () => request.destroy(new Error('RPC timeout')));
    request.end(body);
  });
}

async function main() {
  const input = args();
  if (input.confirm !== confirmPhrase) throw new Error('Exact validator activation confirmation is required.');
  if (!/^validator-[1-4]$/.test(String(input.node || ''))) throw new Error('A single validator node name is required.');
  if (!/^[A-Za-z0-9._:-]{8,128}$/.test(String(input['approved-by'] || ''))) throw new Error('A valid approved-by identifier is required.');
  if (fs.existsSync(approvalPath)) throw new Error('Another validator activation approval is already present.');
  const topology = JSON.parse(fs.readFileSync(topologyPath, 'utf8'));
  const runtime = JSON.parse(fs.readFileSync(runtimeManifestPath, 'utf8'));
  const keyAttestation = JSON.parse(fs.readFileSync(keyAttestationPath, 'utf8'));
  const target = topology.validators.find(row => row.nodeName === input.node);
  const attestedAt = new Date(keyAttestation.verifiedAt || 0).getTime();
  const attestationExpiresAt = new Date(keyAttestation.expiresAt || 0).getTime();
  const attestedEnvelopeManifestSha256 = String(keyAttestation.bindings?.envelopeManifestSha256 || '');
  if (
    !target
    || runtime.contents?.privateKeysIncluded !== false
    || keyAttestation.schemaVersion !== 'plais-mainnet-key-security-attestation/v1'
    || keyAttestation.checks?.encryptedValidatorEnvelopes !== true
    || keyAttestation.checks?.validatorKeyDeliveryLocked !== true
    || !/^[a-f0-9]{64}$/.test(attestedEnvelopeManifestSha256)
    || !Number.isFinite(attestedAt) || attestedAt > Date.now() || Date.now() - attestedAt > 24 * 60 * 60_000
    || !Number.isFinite(attestationExpiresAt) || attestationExpiresAt <= Date.now()
  ) throw new Error('Validator activation bindings are invalid.');

  const pm2Attestation = JSON.parse(fs.readFileSync(pm2StateAttestationPath, 'utf8'));
  const pm2ObservedAt = new Date(pm2Attestation.observedAt || 0).getTime();
  const pm2Rows = pm2Attestation.states || {};
  if (
    pm2Attestation.schemaVersion !== 'plais-validator-pm2-state-attestation/v1'
    || !/^[A-Za-z0-9._:-]{8,128}$/.test(String(pm2Attestation.attestationId || ''))
    || !Number.isFinite(pm2ObservedAt)
    || Date.now() - pm2ObservedAt < 0
    || Date.now() - pm2ObservedAt > 30_000
  ) throw new Error('Fresh PM2 validator state attestation is required.');
  const states = Object.fromEntries([1, 2, 3, 4].map(index => {
    const name = `plais-qbft-validator-${index}`;
    const status = String(pm2Rows[name]?.status || 'missing');
    return [name, status];
  }));
  const targetPm2Name = `plais-qbft-validator-${Number(input.node.split('-')[1])}`;
  if (states[targetPm2Name] !== 'stopped') {
    throw new Error(`Central validator handoff state is unsafe: ${JSON.stringify(states)}`);
  }

  const stateNodes = persistentStore.readState().nodes || [];
  const node = latestNodeForTarget(stateNodes, target);
  const meta = node?.metadata || {};
  const envelope = {
    deviceKeyId: String(meta.provisioningKeyId || ''),
    sha256: String(meta.validatorKeyEnvelopeHash || '')
  };
  const ageMs = Date.now() - new Date(meta.validatorRuntimeCheckedAt || 0).getTime();
  if (
    node?.status !== 'active'
    || Date.now() - new Date(node.lastHeartbeatAt || 0).getTime() > 3 * 60_000
    || !supportedClientVersions.has(node.clientVersion)
    || meta.validatorRuntimeSchema !== 'plais-validator-runtime-deployment-status/v1'
    || meta.validatorRuntimeState !== 'presync-ready'
    || meta.validatorRuntimeArtifactSha256 !== runtime.sha256
    || meta.validatorRuntimeDeviceKeyId !== envelope.deviceKeyId
    || !/^[a-f0-9]{64}$/.test(envelope.deviceKeyId)
    || !/^[a-f0-9]{64}$/.test(envelope.sha256)
    || Number(meta.validatorRuntimeGapBlocks || 0) > 10
    || Number(meta.validatorRuntimePeerCount || 0) < 1
    || meta.validatorTunnelSchema !== 'plais-validator-p2p-tunnel/v1'
    || !['direct-peers-applied', 'direct-lan-quorum-with-tunnel-fallback-applied'].includes(meta.validatorTunnelState)
    || !/^[a-f0-9]{64}$/.test(String(meta.validatorTunnelTopologyHash || ''))
    || !Number.isFinite(ageMs) || ageMs < 0 || ageMs > 3 * 60_000
    || meta.validatorKeyDeliveryState !== 'encrypted-envelope-staged'
  ) throw new Error(`Remote validator target is not presync-ready: ${target.pcName}`);

  const otherAvailability = topology.validators
    .filter(row => row.nodeName !== target.nodeName)
    .map(row => {
      const index = Number(row.nodeName.split('-')[1]);
      const centralPm2Name = `plais-qbft-validator-${index}`;
      const remoteNode = latestNodeForTarget(stateNodes, row);
      const remoteMeta = remoteNode?.metadata || {};
      const heartbeatAgeMs = Date.now() - new Date(remoteNode?.lastHeartbeatAt || 0).getTime();
      const runtimeAgeMs = Date.now() - new Date(remoteMeta.validatorRuntimeCheckedAt || 0).getTime();
      const remoteBlockNumber = Number(remoteMeta.validatorRuntimeBlockNumber || 0);
      const remoteActive = remoteNode?.status === 'active'
        && heartbeatAgeMs >= 0 && heartbeatAgeMs <= 3 * 60_000
        && supportedClientVersions.has(remoteNode?.clientVersion)
        && remoteMeta.validatorRuntimeSchema === 'plais-validator-runtime-deployment-status/v1'
        && remoteMeta.validatorRuntimeState === 'validator-active'
        && remoteMeta.validatorRuntimeArtifactSha256 === runtime.sha256
        && Number(remoteMeta.validatorRuntimeProcessPid || 0) > 0
        && Number(remoteMeta.validatorRuntimePeerCount || 0) >= 1
        && Number(remoteMeta.validatorRuntimeGapBlocks || 0) <= 10
        && Number.isInteger(remoteBlockNumber) && remoteBlockNumber > 0
        && remoteMeta.validatorTunnelSchema === 'plais-validator-p2p-tunnel/v1'
        && ['direct-peers-applied', 'direct-lan-quorum-with-tunnel-fallback-applied'].includes(remoteMeta.validatorTunnelState)
        && /^[a-f0-9]{64}$/.test(String(remoteMeta.validatorTunnelTopologyHash || ''))
        && runtimeAgeMs >= 0 && runtimeAgeMs <= 3 * 60_000;
      const centralOnline = states[centralPm2Name] === 'online';
      return { nodeName: row.nodeName, centralOnline, remoteActive, remoteBlockNumber, available: centralOnline || remoteActive };
    });
  if (otherAvailability.length !== 3 || otherAvailability.some(row => !row.available)) {
    throw new Error(`Validator quorum handoff availability is unsafe: ${JSON.stringify(otherAvailability)}`);
  }

  const before = Number.parseInt(String(await rpc('eth_blockNumber')), 16);
  const blockDeadline = Date.now() + 30_000;
  let after = before;
  while (after <= before && Date.now() < blockDeadline) {
    await sleep(2000);
    after = Number.parseInt(String(await rpc('eth_blockNumber')), 16);
  }
  if (!Number.isInteger(before) || !Number.isInteger(after)) throw new Error('QBFT block height is invalid during validator handoff.');
  const targetBlockNumber = Number(meta.validatorRuntimeBlockNumber || 0);
  const chainAdvancedDuringHandoff = after > before;
  const boundedStalledHandoffSafe = !chainAdvancedDuringHandoff
    && after === before
    && Object.values(states).every(state => state === 'stopped')
    && Number.isInteger(targetBlockNumber)
    && Math.abs(before - targetBlockNumber) <= 10
    && otherAvailability.every(row => row.remoteActive && !row.centralOnline && Math.abs(before - row.remoteBlockNumber) <= 10);
  if (!chainAdvancedDuringHandoff && !boundedStalledHandoffSafe) {
    throw new Error('QBFT handoff is stalled without four aligned remote validator processes.');
  }

  const approvedAt = new Date();
  const approval = {
    schemaVersion: 'plais-validator-activation-approval/v1',
    action: confirmPhrase,
    approvalId: `validator-activation-${input.node}-${approvedAt.getTime()}-${crypto.randomBytes(5).toString('hex')}`,
    approvedBy: input['approved-by'],
    approvedAt: approvedAt.toISOString(),
    expiresAt: new Date(approvedAt.getTime() + 15 * 60_000).toISOString(),
    nodeName: target.nodeName,
    pcName: target.pcName,
    deviceKeyId: envelope.deviceKeyId,
    validatorAddress: target.validatorAddress,
    topologySha256: sha256File(topologyPath),
    envelopeManifestSha256: attestedEnvelopeManifestSha256,
    runtimeManifestSha256: sha256File(runtimeManifestPath),
    envelopeSha256: envelope.sha256,
    runtimeArtifactSha256: runtime.sha256,
    centralValidatorStopped: true,
    remainingCentralValidatorCount: Object.values(states).filter(state => state === 'online').length,
    availableOtherValidators: otherAvailability,
    pm2StateAttestationId: pm2Attestation.attestationId,
    pm2StateObservedAt: pm2Attestation.observedAt,
    blockBefore: before,
    blockAfter: after,
    chainAdvancedDuringHandoff,
    handoffMode: chainAdvancedDuringHandoff ? 'live-three-validator-handoff' : 'bounded-stall-four-aligned-remote-handoff',
    controls: {
      validatorKeyDecryptAllowed: true,
      validatorKeyImportAllowed: true,
      validatorStartAllowed: true,
      publicRpcAllowed: false,
      externalTransactionAllowed: false,
      operatorSigningAllowed: false,
      arbitraryCommandAllowed: false
    }
  };
  const tempPath = `${approvalPath}.tmp-${process.pid}`;
  fs.writeFileSync(tempPath, JSON.stringify(approval, null, 2), { encoding: 'utf8', flag: 'wx' });
  if (!restrictAcl(tempPath, false).ok) throw new Error('Validator activation approval ACL restriction failed.');
  fs.renameSync(tempPath, approvalPath);
  if (!auditAcl(approvalPath).ok) throw new Error('Validator activation approval ACL audit failed.');
  console.log(JSON.stringify({ ok: true, approvalId: approval.approvalId, nodeName: target.nodeName, pcName: target.pcName, blockBefore: before, blockAfter: after, expiresAt: approval.expiresAt, controls: approval.controls }, null, 2));
}

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