require('../config/loadEnv')();
const fs = require('fs');
const http = require('http');
const path = require('path');
const { spawnSync } = require('child_process');
const persistentStore = require('../services/persistentStore');
const topologyService = require('../services/validatorTunnelTopologyService');
const { matchesValidatorTarget } = require('../services/validatorTargetBindingService');

const CONFIRMATION = 'FINALIZE CENTRAL VALIDATOR TRANSPORT OUTAGE CANARY';
const confirm = String(process.argv.find(value => value.startsWith('--confirm=')) || '').slice(10);
if (confirm !== CONFIRMATION) throw new Error('Exact outage canary finalization confirmation is required.');

const root = path.join(__dirname, '..');
const evidencePath = path.join(root, 'runtime', 'plais-qbft-canonical-migration', 'direct-lan-outage-canary.json');
const targetNodeNames = ['validator-1', 'validator-2', 'validator-3'];
const expectedVersion = '1.4.123-validator-direct-control-heartbeat-diagnostic-tunnel-fallback-locked';
if (fs.existsSync(topologyService.paths.approvalPath)) {
  throw new Error('No validator approval may be active during outage evidence finalization.');
}
const topology = JSON.parse(fs.readFileSync(topologyService.paths.topologyPath, 'utf8'));
const direct = topologyService.getDirectQuorum(topology);
const report = JSON.parse(fs.readFileSync(evidencePath, 'utf8'));
const stoppedAt = new Date(report.stoppedAt || 0).getTime();
const restoredAt = new Date(report.restoredAt || 0).getTime();
const outageSamples = Array.isArray(report.outageSamples) ? report.outageSamples : [];
const outageSamplesValid = outageSamples.length === 3
  && outageSamples.every((sample, index) => sample.elapsedSeconds === (index + 1) * 30
    && sample.aisforStatus === 'stopped' && sample.publicBridgeStatus === 'stopped'
    && new Date(sample.observedAt || 0).getTime() >= stoppedAt + sample.elapsedSeconds * 1000
    && new Date(sample.observedAt || 0).getTime() <= restoredAt);
if (!direct || direct.state !== 'ACTIVE'
  || JSON.stringify(direct.targetNodeNames) !== JSON.stringify(targetNodeNames)
  || report.schemaVersion !== 'plais-validator-direct-lan-outage-canary/v1'
  || report.status !== 'FAILED' || report.ok !== false
  || JSON.stringify(report.targetNodeNames) !== JSON.stringify(targetNodeNames)
  || report.centralProcessesStoppedTogether !== true
  || report.centralProcessesRestored !== true
  || Number(report.outageSeconds || 0) < 90
  || !Number.isFinite(stoppedAt) || stoppedAt <= 0
  || !Number.isFinite(restoredAt) || restoredAt <= stoppedAt
  || Math.abs((restoredAt - stoppedAt) / 1000 - Number(report.outageSeconds || 0)) > 2
  || !outageSamplesValid
  || JSON.stringify(report.activationEvidence || {}) !== JSON.stringify(direct.activationEvidence || {})) {
  throw new Error('The failed-but-restored outage canary evidence is not eligible for finalization.');
}
const baselineRows = Array.isArray(report.baselineRows) ? report.baselineRows : [];
const baselineRpcBlock = Number(report.baselineRpcBlock || 0);
if (!Number.isFinite(stoppedAt) || stoppedAt <= 0 || baselineRows.length !== 3 || baselineRpcBlock <= 0
  || baselineRows.some(row => Number(row.verifiedAgainstRpcBlock || 0) <= 0
    || Number(row.blockNumber || 0) < Number(row.verifiedAgainstRpcBlock || 0) - 10
    || new Date(row.verifiedAt || 0).getTime() > stoppedAt
    || new Date(row.verifiedAt || 0).getTime() < stoppedAt - 3 * 60_000)) {
  throw new Error('The outage baseline or RPC progress evidence is incomplete.');
}

function rpc(method, params = []) {
  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: '/', timeout: 5000,
      headers: { 'Content-Type': 'application/json', 'Content-Length': body.length } }, response => {
      const chunks = [];
      response.on('data', chunk => chunks.push(chunk));
      response.on('end', () => {
        try {
          const payload = JSON.parse(Buffer.concat(chunks).toString('utf8'));
          if (payload.error) throw new Error(payload.error.message || 'RPC error');
          resolve(payload.result);
        }
        catch (error) { reject(error); }
      });
    });
    request.on('timeout', () => request.destroy(new Error('RPC timeout')));
    request.on('error', reject);
    request.end(body);
  });
}

async function rpcBlockNumber() {
  return Number.parseInt(await rpc('eth_blockNumber'), 16);
}

async function buildOutageBlockProof(postRpcBlock) {
  if (postRpcBlock - baselineRpcBlock > 5000) throw new Error('Outage block proof range is unexpectedly large.');
  const blocks = [];
  for (let blockNumber = baselineRpcBlock + 1; blockNumber <= postRpcBlock; blockNumber += 1) {
    const header = await rpc('eth_getBlockByNumber', [`0x${blockNumber.toString(16)}`, false]);
    if (!header) continue;
    const timestampMs = Number.parseInt(header.timestamp, 16) * 1000;
    if (timestampMs >= stoppedAt && timestampMs <= restoredAt) {
      blocks.push({ blockNumber, timestamp: new Date(timestampMs).toISOString(), hash: String(header.hash || '') });
    }
  }
  const firstTimestampMs = new Date(blocks[0]?.timestamp || 0).getTime();
  const lastTimestampMs = new Date(blocks.at(-1)?.timestamp || 0).getTime();
  const spanSeconds = blocks.length > 1 ? Math.round((lastTimestampMs - firstTimestampMs) / 1000) : 0;
  const maxBlockGapSeconds = blocks.length > 1
    ? Math.max(...blocks.slice(1).map((block, index) => Math.round(
      (new Date(block.timestamp).getTime() - new Date(blocks[index].timestamp).getTime()) / 1000))) : 0;
  return {
    method: 'post-restore-canonical-block-header-timestamps',
    blocks,
    blockCount: blocks.length,
    spanSeconds,
    maxBlockGapSeconds,
    firstBlockWithin30Seconds: blocks.length > 0 && firstTimestampMs <= stoppedAt + 30_000,
    lastBlockAfter75Seconds: blocks.length > 0 && lastTimestampMs >= stoppedAt + 75_000,
    valid: blocks.length >= 3 && spanSeconds >= 60 && maxBlockGapSeconds <= 60
      && firstTimestampMs <= stoppedAt + 30_000
      && lastTimestampMs >= stoppedAt + 75_000
  };
}

function pm2States() {
  const appData = String(process.env.APPDATA || '');
  const cliPath = process.platform === 'win32'
    ? path.join(appData, 'npm', 'node_modules', 'pm2', 'bin', 'pm2') : '';
  const command = process.platform === 'win32' ? process.execPath : 'pm2';
  const args = process.platform === 'win32' ? [cliPath, 'jlist'] : ['jlist'];
  if (process.platform === 'win32' && !fs.existsSync(cliPath)) throw new Error('Windows PM2 CLI is unavailable.');
  const result = spawnSync(command, args, { cwd: root, encoding: 'utf8', windowsHide: true, timeout: 30_000 });
  if (result.status !== 0) throw new Error('PM2 state verification failed.');
  return Object.fromEntries(JSON.parse(String(result.stdout || '[]'))
    .map(row => [row.name, String(row.pm2_env?.status || '')]));
}

async function main() {
  const states = pm2States();
  if (states.aisfor !== 'online' || states['plais-qbft-public-bridge'] !== 'online') {
    throw new Error('Central services are not online after the outage canary.');
  }
  const currentRpcBlock = await rpcBlockNumber();
  if (currentRpcBlock < baselineRpcBlock + 10) throw new Error('Current RPC head did not preserve outage progress.');
  const outageBlockProof = await buildOutageBlockProof(currentRpcBlock);
  if (outageBlockProof.valid !== true) throw new Error('Canonical block headers do not prove sustained production during the outage.');
  const nodes = persistentStore.readFreshState().nodes || [];
  const now = Date.now();
  const postRows = direct.targets.map(target => {
    const node = nodes.filter(row => matchesValidatorTarget(row, target))
      .sort((a, b) => new Date(b.lastHeartbeatAt || 0) - new Date(a.lastHeartbeatAt || 0))[0];
    const meta = node?.metadata || {};
    const baseline = baselineRows.find(row => row.nodeName === target.nodeName);
    const heartbeatAtMs = new Date(node?.lastHeartbeatAt || 0).getTime();
    const row = {
      nodeName: target.nodeName,
      pcName: target.pcName,
      heartbeatAt: node?.lastHeartbeatAt || null,
      processPid: Number(meta.validatorRuntimeProcessPid || 0),
      blockNumber: Number(meta.validatorRuntimeBlockNumber || 0),
      peerCount: Number(meta.validatorRuntimePeerCount || 0),
      clientVersion: String(node?.clientVersion || '')
    };
    if (node?.status !== 'active'
      || row.clientVersion !== expectedVersion
      || !Number.isFinite(heartbeatAtMs) || heartbeatAtMs <= restoredAt || heartbeatAtMs > now
      || now - heartbeatAtMs > 10 * 60_000
      || row.processPid <= 0 || row.processPid !== Number(baseline?.processPid || 0)
      || row.blockNumber < Number(baseline?.blockNumber || 0) + 10 || row.peerCount < 2) {
      throw new Error(`Replicated post-outage validator evidence is incomplete: ${target.nodeName}.`);
    }
    return row;
  });
  const finalized = {
    ...report,
    status: 'PASSED',
    ok: true,
    postRows,
    outageBlockProof,
    postRpcBlock: Math.max(Number(report.postRpcBlock || 0), currentRpcBlock),
    finalizedAfterReplicationDelay: true,
    checkedAt: new Date().toISOString()
  };
  const temporaryPath = `${evidencePath}.${process.pid}.tmp`;
  fs.writeFileSync(temporaryPath, `${JSON.stringify(finalized, null, 2)}\n`, { encoding: 'utf8', flag: 'wx' });
  fs.renameSync(temporaryPath, evidencePath);
  console.log(JSON.stringify(finalized, null, 2));
}

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