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

const fs = require('fs');
const crypto = require('crypto');
const path = require('path');
const { spawnSync } = require('child_process');
const { EVM_CANDIDATE_CONFIG } = require('../config/evmCandidateConfig');
const conversionService = require('../services/canonicalPointConversionService');

const projectRoot = path.join(__dirname, '..');
const migrationRoot = path.join(projectRoot, 'runtime', 'plais-qbft-canonical-migration');

function parseJsonOutput(output) {
  const text = String(output || '').trim();
  const first = text.indexOf('{');
  const last = text.lastIndexOf('}');
  if (first < 0 || last < first) return null;
  try { return JSON.parse(text.slice(first, last + 1)); } catch (_) { return null; }
}

function runProbe(scriptName, attempts = 1) {
  let last = null;
  for (let attempt = 1; attempt <= attempts; attempt += 1) {
    const result = spawnSync(process.execPath, [path.join(__dirname, scriptName)], {
      cwd: projectRoot,
      encoding: 'utf8',
      windowsHide: true,
      maxBuffer: 4 * 1024 * 1024,
      timeout: 45_000
    });
    const data = parseJsonOutput(result.stdout || result.stderr);
    last = data || { ok: false, error: result.error?.code || 'PROBE_OUTPUT_INVALID' };
    if (last.ok === true) return last;
  }
  return last;
}

function readJson(relativePath, fallback = null) {
  try {
    return JSON.parse(fs.readFileSync(path.join(projectRoot, relativePath), 'utf8').replace(/^\uFEFF/, ''));
  } catch (_) { return fallback; }
}

function check(key, ok, detail) {
  return { key, ok: ok === true, detail };
}

function directActivationEvidenceValid(direct, runtimeRows) {
  const expectedNodes = ['validator-1', 'validator-2', 'validator-3'];
  const expectedVersion = '1.4.123-validator-direct-control-heartbeat-diagnostic-tunnel-fallback-locked';
  const activation = direct?.activationEvidence;
  const evidenceNodes = Array.isArray(activation?.nodes) ? activation.nodes : [];
  if (direct?.state !== 'ACTIVE'
    || JSON.stringify(direct?.targetNodeNames || []) !== JSON.stringify(expectedNodes)
    || activation?.schemaVersion !== 'plais-validator-direct-quorum-activation-evidence/v1'
    || !/^[a-f0-9]{64}$/.test(String(activation?.stagedTopologySha256 || ''))
    || JSON.stringify(evidenceNodes.map(row => row.nodeName)) !== JSON.stringify(expectedNodes)
    || new Set(evidenceNodes.map(row => row.approvalId)).size !== 3) return false;

  const historyRoot = path.join(migrationRoot, 'approval-history');
  return expectedNodes.every(nodeName => {
    const bound = evidenceNodes.find(row => row.nodeName === nodeName);
    const runtime = runtimeRows.find(row => row.nodeName === nodeName);
    if (!bound || !runtime || path.basename(String(bound.archiveFile || '')) !== bound.archiveFile) return false;
    try {
      const archiveBytes = fs.readFileSync(path.join(historyRoot, bound.archiveFile));
      const archiveSha256 = crypto.createHash('sha256').update(archiveBytes).digest('hex');
      const archive = JSON.parse(archiveBytes);
      const evidence = archive?.evidence || {};
      return archiveSha256 === bound.archiveSha256
        && archive.archiveStatus === 'DIRECT_CONTROL_ATTESTED'
        && archive.action === 'ATTEST_EXISTING_SINGLE_VALIDATOR_DIRECT_LAN'
        && archive.approvalId === bound.approvalId
        && archive.nodeName === nodeName
        && archive.topologySha256 === activation.stagedTopologySha256
        && evidence.schemaVersion === 'plais-validator-direct-control-evidence/v1'
        && evidence.nodeName === nodeName
        && evidence.clientVersion === expectedVersion
        && evidence.topologySha256 === activation.stagedTopologySha256
        && evidence.managerState === 'applied'
        && evidence.runtimeState === 'validator-active'
        && evidence.directStatic === true
        && evidence.processRunning === true
        && evidence.offerAuditObserved === true
        && evidence.authenticatedHeartbeatObserved === true
        && Number(evidence.processPid || 0) === Number(bound.processPid || 0)
        && runtime.clientVersion === expectedVersion
        && runtime.ready === true
        && Number(runtime.processPid || 0) === Number(bound.processPid || 0)
        && Number(runtime.blockNumber || 0) >= Number(bound.currentBlockNumber || 0)
        && Number(runtime.peerCount || 0) >= 2;
    } catch (_) { return false; }
  });
}

function outageEvidenceValid(evidence, direct) {
  const activatedAt = new Date(direct?.activatedAt || 0).getTime();
  const checkedAt = new Date(evidence?.checkedAt || 0).getTime();
  const stoppedAt = new Date(evidence?.stoppedAt || 0).getTime();
  const restoredAt = new Date(evidence?.restoredAt || 0).getTime();
  const baseline = Array.isArray(evidence?.baselineRows) ? evidence.baselineRows : [];
  const post = Array.isArray(evidence?.postRows) ? evidence.postRows : [];
  const samples = Array.isArray(evidence?.outageSamples) ? evidence.outageSamples : [];
  const outageBlocks = Array.isArray(evidence?.outageBlockProof?.blocks) ? evidence.outageBlockProof.blocks : [];
  const blockTimes = outageBlocks.map(block => new Date(block.timestamp || 0).getTime());
  const targets = ['validator-1', 'validator-2', 'validator-3'];
  return evidence?.schemaVersion === 'plais-validator-direct-lan-outage-canary/v1'
    && evidence.ok === true
    && evidence.status === 'PASSED'
    && evidence.centralProcessesStoppedTogether === true
    && evidence.centralProcessesRestored === true
    && JSON.stringify(evidence.targetNodeNames) === JSON.stringify(targets)
    && Number.isFinite(activatedAt) && activatedAt > 0
    && Number.isFinite(checkedAt) && checkedAt >= activatedAt
    && Number.isFinite(stoppedAt) && stoppedAt > 0
    && Number.isFinite(restoredAt) && restoredAt > stoppedAt
    && Number(evidence.outageSeconds || 0) >= 90
    && Math.abs((restoredAt - stoppedAt) / 1000 - Number(evidence.outageSeconds || 0)) <= 2
    && samples.length === 3
    && samples.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)
    && evidence.outageBlockProof?.valid === true
    && Number(evidence.outageBlockProof?.blockCount || 0) === outageBlocks.length
    && outageBlocks.length >= 3
    && Number(evidence.outageBlockProof?.spanSeconds || 0) >= 60
    && Number(evidence.outageBlockProof?.maxBlockGapSeconds || 0) <= 60
    && outageBlocks.every((block, index) => Number(block.blockNumber || 0) > Number(evidence.baselineRpcBlock || 0)
      && /^0x[a-f0-9]{64}$/i.test(String(block.hash || ''))
      && Number.isFinite(blockTimes[index]) && blockTimes[index] >= stoppedAt && blockTimes[index] <= restoredAt
      && (index === 0 || Number(block.blockNumber) > Number(outageBlocks[index - 1].blockNumber)))
    && blockTimes[0] <= stoppedAt + 30_000
    && blockTimes.at(-1) >= stoppedAt + 75_000
    && baseline.length === 3 && post.length === 3
    && targets.every((nodeName) => {
      const before = baseline.find(row => row.nodeName === nodeName);
      const after = post.find(row => row.nodeName === nodeName);
      return Number(before?.processPid || 0) > 0
        && Number(before?.verifiedAgainstRpcBlock || 0) > 0
        && Number(before?.blockNumber || 0) >= Number(before?.verifiedAgainstRpcBlock || 0) - 10
        && new Date(before?.verifiedAt || 0).getTime() <= stoppedAt
        && new Date(before?.verifiedAt || 0).getTime() >= stoppedAt - 3 * 60_000
        && Number(after?.processPid || 0) === Number(before.processPid)
        && Number(after?.blockNumber || 0) >= Number(before?.blockNumber || 0) + 10
        && Number(after?.peerCount || 0) >= 2;
    });
}

function main() {
  const chain = runProbe('evm-network-self-test.js', 2);
  const validators = runProbe('validator-runtime-deployment-status.js');
  const hosts = runProbe('qbft-host-distribution-check.js');
  const fleet = runProbe('sync-client-fleet-readiness.js');
  const topology = readJson('runtime/plais-qbft-canonical-migration/topology.json', {});
  const outage = readJson('runtime/plais-qbft-canonical-migration/direct-lan-outage-canary.json', null);
  const direct = topology.directQuorum || {};
  const directRows = (validators.rows || []).filter(row =>
    ['validator-1', 'validator-2', 'validator-3'].includes(row.nodeName));
  const directEvidenceReady = directActivationEvidenceValid(direct, directRows);
  const conversion = conversionService.getReadiness();
  const conversionChecksExceptLocalCustodyRead = conversion.checks
    .filter(row => row.key !== 'operator_custody');
  // The long-running signing worker executes the same operator-custody gate before
  // every heartbeat. A fresh ready heartbeat is privileged runtime evidence when
  // this read-only audit shell is intentionally denied direct vault ACL access.
  const pointConversionOperational = Number(conversion.rate) === 1000
    && conversion.worker?.online === true
    && conversionChecksExceptLocalCustodyRead.every(row => row.ok);
  const liveSecurityControls = {
    managementApiLocked: process.env.AISFOR_CHAIN_PUBLIC_API_LOCK === '1',
    serviceAuthenticationRequired: process.env.AISFOR_CHAIN_AUTH_REQUIRED === '1',
    hmacV2Required: process.env.AISFOR_CHAIN_REQUIRE_HMAC_V2 === '1',
    legacyMockWritesLocked: process.env.AISFOR_CHAIN_MOCK_BLOCK_WRITES_ENABLED === '0',
    externalEvmTransfersOperational: EVM_CANDIDATE_CONFIG.externalTransfersEnabled === true,
    operatorApiSigningLocked: process.env.PLAIS_EVM_OPERATOR_SIGNING_ENABLED === '0',
    multichainTransfersLocked: process.env.PLAIS_MULTICHAIN_TRANSFERS_ENABLED === '0'
  };

  const checks = [
    check(
      'live_chain_liveness',
      chain.ok === true && Number(chain.chainId) === 20260718
        && Number(chain.validators) === 4 && Number(chain.advancedBy) > 0,
      `chainId ${chain.chainId || 'unknown'}, validators ${chain.validators || 0}, peers ${chain.peers ?? 'unknown'}, block advance ${chain.advancedBy || 0}.`
    ),
    check(
      'remote_validator_runtime_fleet',
      validators.allReady === true && Number(validators.readyCount) === 4
        && (validators.rows || []).every(row => Number(row.processPid || 0) > 0),
      `${validators.readyCount || 0}/4 remote validator runtimes are fresh and ready.`
    ),
    check(
      'independent_validator_host_identities',
      hosts.ok === true && Number(hosts.independentValidatorHostCount || 0) >= 4,
      `${hosts.independentValidatorHostCount || 0}/${hosts.requiredIndependentHosts || 4} distinct validator device identities are live.`
    ),
    check(
      'direct_validator_quorum_active',
      directEvidenceReady,
      directEvidenceReady
        ? '3/3 LAN validators match immutable direct-control archives and current runtime PIDs.'
        : `Direct-control activation evidence is incomplete; topology state ${direct.state || 'missing'}.`
    ),
    check(
      'central_transport_outage_canary',
      outageEvidenceValid(outage, direct),
      outage?.status === 'PASSED'
        ? `Central transport outage evidence exists (${outage.checkedAt || 'unknown'}).`
        : 'A controlled 90-second central AISFOR/public-bridge outage canary has not been completed after direct quorum activation.'
    ),
    check(
      'point_conversion_1000_to_1',
      pointConversionOperational,
      `rate ${conversion.rate || 'unknown'}P:1 PLAIS; worker ${conversion.worker?.online === true ? 'online' : 'offline'}; non-custody controls ${conversionChecksExceptLocalCustodyRead.filter(row => row.ok).length}/${conversionChecksExceptLocalCustodyRead.length}; fresh ready worker heartbeat is the privileged custody-gate evidence.`
    ),
    check(
      'live_mainnet_security_controls',
      Object.values(liveSecurityControls).every(Boolean),
      `${Object.values(liveSecurityControls).filter(Boolean).length}/${Object.keys(liveSecurityControls).length} live-mainnet controls match policy.`
    ),
    check(
      'sync_client_fleet',
      fleet.ready === true && Number(fleet.validatorCandidateCount || 0) >= 4,
      `${fleet.activeDeviceCount || 0} active clients; ${fleet.validatorCandidateCount || 0} validator candidates.`
    )
  ];
  const blockers = checks.filter(row => !row.ok);
  const report = {
    ok: blockers.length === 0,
    schemaVersion: 'plais-live-mainnet-listing-gate/v1',
    status: blockers.length === 0 ? 'TECHNICALLY_READY_FOR_EXCHANGE_DUE_DILIGENCE' : 'REMEDIATION_REQUIRED',
    scope: 'Read-only live-mainnet operational gate. No transaction, signer activation, service outage, or validator restart is performed.',
    chainId: 20260718,
    technicalPreconditionsMet: blockers.length === 0,
    passed: checks.length - blockers.length,
    total: checks.length,
    blockers: blockers.map(row => ({ key: row.key, detail: row.detail })),
    checks,
    controls: liveSecurityControls,
    checkedAt: new Date().toISOString()
  };
  console.log(JSON.stringify(report, null, 2));
  if (!report.ok) process.exitCode = 1;
}

main();
