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 = 'CANARY STOP CENTRAL VALIDATOR TRANSPORT FOR 90 SECONDS';
const confirm = String(process.argv.find(value => value.startsWith('--confirm=')) || '').slice(10);
if (confirm !== CONFIRMATION) throw new Error('Exact central validator transport outage confirmation is required.');

const evidencePath = path.join(__dirname, '..', 'runtime', 'plais-qbft-canonical-migration', 'direct-lan-outage-canary.json');
if (fs.existsSync(topologyService.paths.approvalPath)) {
  throw new Error('No validator restart or attestation approval may be active during the outage canary.');
}
const topology = JSON.parse(fs.readFileSync(topologyService.paths.topologyPath, 'utf8'));
const direct = topologyService.getDirectQuorum(topology);
const activation = direct?.activationEvidence;
if (!direct || direct.state !== 'ACTIVE'
  || activation?.schemaVersion !== 'plais-validator-direct-quorum-activation-evidence/v1'
  || JSON.stringify((activation.nodes || []).map(row => row.nodeName))
    !== JSON.stringify(['validator-1', 'validator-2', 'validator-3'])) {
  throw new Error('A control-evidence-bound active direct LAN quorum is required.');
}

const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
const pm2CliPath = process.platform === 'win32'
  ? path.join(String(process.env.APPDATA || ''), 'npm', 'node_modules', 'pm2', 'bin', 'pm2')
  : '';
const pm2Command = process.platform === 'win32' ? process.execPath : 'pm2';

function pm2(args, allowFailure = false) {
  if (process.platform === 'win32' && !fs.existsSync(pm2CliPath)) {
    throw new Error('The Windows PM2 CLI entry point is unavailable.');
  }
  const commandArgs = process.platform === 'win32' ? [pm2CliPath, ...args] : args;
  const result = spawnSync(pm2Command, commandArgs, { cwd: path.join(__dirname, '..'), encoding: 'utf8', windowsHide: true, timeout: 30_000 });
  if (!allowFailure && result.status !== 0) throw new Error(`PM2 ${args.join(' ')} failed: ${String(result.stderr || result.stdout || '').slice(0, 500)}`);
  return result;
}

function pm2States() {
  const result = pm2(['jlist']);
  const rows = JSON.parse(String(result.stdout || '[]'));
  return Object.fromEntries(rows.map(row => [row.name, String(row.pm2_env?.status || '')]));
}

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(baselineRpcBlock, postRpcBlock, stoppedAt, restoredAt) {
  if (postRpcBlock - baselineRpcBlock > 5000) throw new Error('Outage block proof range is unexpectedly large.');
  const stoppedAtMs = stoppedAt.getTime();
  const restoredAtMs = restoredAt.getTime();
  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 >= stoppedAtMs && timestampMs <= restoredAtMs) {
      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 <= stoppedAtMs + 30_000,
    lastBlockAfter75Seconds: blocks.length > 0 && lastTimestampMs >= stoppedAtMs + 75_000,
    valid: blocks.length >= 3 && spanSeconds >= 60 && maxBlockGapSeconds <= 60
      && firstTimestampMs <= stoppedAtMs + 30_000
      && lastTimestampMs >= stoppedAtMs + 75_000
  };
}

function latestRows() {
  const nodes = persistentStore.readFreshState().nodes || [];
  return 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 || {};
    return {
      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 || '')
    };
  });
}

function writeEvidence(value) {
  const temporaryPath = `${evidencePath}.${process.pid}.tmp`;
  fs.writeFileSync(temporaryPath, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', flag: 'wx' });
  fs.renameSync(temporaryPath, evidencePath);
}

async function main() {
  const states = pm2States();
  if (states.aisfor !== 'online' || states['plais-qbft-public-bridge'] !== 'online') {
    throw new Error('Central validator transport processes must both be online before the canary.');
  }
  const baselineDeadline = Date.now() + 3 * 60_000;
  const verifiedBaselineRows = new Map();
  let baselineRows = [];
  let baselineRpcBlock = 0;
  while (Date.now() < baselineDeadline) {
    const observedRows = latestRows();
    const observedRpcBlock = await rpcBlockNumber();
    for (const row of observedRows) {
      if (Date.now() - new Date(row.heartbeatAt || 0).getTime() <= 90_000
        && row.processPid > 0 && row.peerCount >= 2
        && row.clientVersion === '1.4.123-validator-direct-control-heartbeat-diagnostic-tunnel-fallback-locked'
        && row.blockNumber >= observedRpcBlock - 10) {
        verifiedBaselineRows.set(row.nodeName, { ...row, verifiedAgainstRpcBlock: observedRpcBlock,
          verifiedAt: new Date().toISOString() });
      }
    }
    if (verifiedBaselineRows.size === 3) {
      baselineRows = direct.targetNodeNames.map(nodeName => verifiedBaselineRows.get(nodeName));
      baselineRpcBlock = await rpcBlockNumber();
      break;
    }
    await sleep(5000);
  }
  if (baselineRows.length !== 3
    || baselineRows.some(row => Date.now() - new Date(row.verifiedAt || 0).getTime() > 3 * 60_000
      || row.processPid <= 0 || row.peerCount < 2
      || row.clientVersion !== '1.4.123-validator-direct-control-heartbeat-diagnostic-tunnel-fallback-locked'
      || row.blockNumber < row.verifiedAgainstRpcBlock - 10)) {
    throw new Error('Each direct validator did not independently converge with the RPC head within three minutes.');
  }
  for (const target of direct.targets) {
    const node = (persistentStore.readFreshState().nodes || []).filter(row => matchesValidatorTarget(row, target))
      .sort((a, b) => new Date(b.lastHeartbeatAt || 0) - new Date(a.lastHeartbeatAt || 0))[0];
    const offer = topologyService.buildOffer(node);
    if (!offer.ok || offer.offer?.directMesh?.enabled !== true || offer.offer?.directMesh?.peers?.length !== 2
      || offer.offer?.controls?.directMeshActivationAllowed !== true
      || offer.offer?.controls?.validatorProcessRestartAllowed !== false
      || offer.offer?.controls?.validatorRestartApprovalId !== '') {
      throw new Error(`Direct offer baseline is incomplete: ${target.nodeName}.`);
    }
  }
  const stoppedAt = new Date();
  let restored = false;
  let restoredAt = null;
  const outageSamples = [];
  const evidence = { schemaVersion: 'plais-validator-direct-lan-outage-canary/v1', status: 'IN_PROGRESS',
    activationEvidence: activation, targetNodeNames: direct.targetNodeNames, baselineRows, baselineRpcBlock,
    centralProcesses: ['aisfor', 'plais-qbft-public-bridge'], stoppedAt: stoppedAt.toISOString(), outageSamples };
  writeEvidence(evidence);
  console.log(JSON.stringify({ ok: true, status: 'CENTRAL_VALIDATOR_TRANSPORT_STOPPING', baselineRpcBlock, baselineRows }));
  try {
    pm2(['stop', 'plais-qbft-public-bridge']);
    pm2(['stop', 'aisfor']);
    const stoppedStates = pm2States();
    if (stoppedStates.aisfor !== 'stopped' || stoppedStates['plais-qbft-public-bridge'] !== 'stopped') {
      throw new Error('Central validator transport stop was not confirmed.');
    }
    for (let elapsed = 30; elapsed <= 90; elapsed += 30) {
      await sleep(30_000);
      const sampleStates = pm2States();
      const sample = {
        elapsedSeconds: elapsed,
        observedAt: new Date().toISOString(),
        aisforStatus: sampleStates.aisfor,
        publicBridgeStatus: sampleStates['plais-qbft-public-bridge'],
        observerRpcBlock: await rpcBlockNumber()
      };
      if (sample.aisforStatus !== 'stopped' || sample.publicBridgeStatus !== 'stopped') {
        throw new Error(`Central validator transport was not continuously stopped at ${elapsed} seconds.`);
      }
      outageSamples.push(sample);
      writeEvidence({ ...evidence, outageSamples });
      console.log(JSON.stringify({ ok: true, status: 'CENTRAL_VALIDATOR_TRANSPORT_OUTAGE_ACTIVE', ...sample }));
    }
  } finally {
    pm2(['start', 'plais-qbft-public-bridge'], true);
    pm2(['start', 'aisfor'], true);
    const restoreDeadline = Date.now() + 60_000;
    while (Date.now() < restoreDeadline) {
      const live = pm2States();
      if (live.aisfor === 'online' && live['plais-qbft-public-bridge'] === 'online') {
        restored = true;
        restoredAt = new Date();
        break;
      }
      await sleep(2000);
    }
  }
  if (!restored) throw new Error('Central validator transport restore was not confirmed.');
  const postDeadline = Date.now() + 3 * 60_000;
  let postRows = [];
  while (Date.now() < postDeadline) {
    postRows = latestRows();
    const fresh = postRows.every(row => Date.now() - new Date(row.heartbeatAt || 0).getTime() <= 90_000);
    const samePids = postRows.every(row => row.processPid === baselineRows.find(base => base.nodeName === row.nodeName)?.processPid);
    const progressed = postRows.every(row => row.blockNumber
      >= Number(baselineRows.find(base => base.nodeName === row.nodeName)?.blockNumber || 0) + 10);
    if (fresh && samePids && progressed && postRows.every(row => row.peerCount >= 2)) break;
    await sleep(5000);
  }
  const postRpcBlock = await rpcBlockNumber();
  const outageSeconds = Math.round((restoredAt.getTime() - stoppedAt.getTime()) / 1000);
  const outageBlockProof = await buildOutageBlockProof(baselineRpcBlock, postRpcBlock, stoppedAt, restoredAt);
  const samplesComplete = outageSamples.length === 3
    && outageSamples.every((sample, index) => sample.elapsedSeconds === (index + 1) * 30
      && sample.aisforStatus === 'stopped' && sample.publicBridgeStatus === 'stopped');
  const complete = samplesComplete && outageBlockProof.valid === true && postRows.length === 3
    && postRows.every(row => row.processPid === baselineRows.find(base => base.nodeName === row.nodeName)?.processPid)
    && postRows.every(row => row.blockNumber >= Number(baselineRows.find(base => base.nodeName === row.nodeName)?.blockNumber || 0) + 10
      && row.peerCount >= 2)
    && postRpcBlock >= baselineRpcBlock + 10;
  const report = { ...evidence, status: complete ? 'PASSED' : 'FAILED', ok: complete, outageSeconds,
    restoredAt: restoredAt.toISOString(), outageSamples, outageBlockProof,
    centralProcessesStoppedTogether: true, centralProcessesRestored: restored, postRows, postRpcBlock,
    checkedAt: new Date().toISOString() };
  writeEvidence(report);
  console.log(JSON.stringify(report, null, 2));
  if (!complete) process.exitCode = 2;
}

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