require('../config/loadEnv')();
const fs = require('fs');
const crypto = require('crypto');
const net = require('net');
const os = require('os');
const path = require('path');
const persistentStore = require('../services/persistentStore');
const { matchesValidatorTarget } = require('../services/validatorTargetBindingService');

const projectRoot = path.join(__dirname, '..');
const topologyPath = path.join(projectRoot, 'runtime', 'plais-qbft-canonical-migration', 'topology.json');
const publicBridgeRoot = path.join(projectRoot, 'runtime', 'plais-qbft-public-bridge');
const publicBridgeConfigPath = path.join(publicBridgeRoot, 'config.json');
const publicBridgeKeyPath = path.join(publicBridgeRoot, 'data', 'key.pub');
const publicBridgeSyncAttestationPath = path.join(publicBridgeRoot, 'sync-attestation.json');
const supportedTunnelClientVersions = 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.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'
]);
// The two central validators that remain during the sequential handoff are
// enough for the RPC-disabled bridge to follow the canonical head. Four local
// peers only existed before validators 1 and 2 moved to remote hosts.
const minPublicBridgePeers = 2;

function sha256File(filePath) {
  return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
}

function privateOverlayIpv4(value) {
  const address = String(value || '').trim();
  if (net.isIP(address) !== 4) return false;
  const parts = address.split('.').map(Number);
  return parts[0] === 10
    || (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31)
    || (parts[0] === 192 && parts[1] === 168)
    || (parts[0] === 100 && parts[1] >= 64 && parts[1] <= 127);
}

function publicIpv4(value) {
  const address = String(value || '').trim();
  if (net.isIP(address) !== 4) return false;
  const parts = address.split('.').map(Number);
  return parts[0] !== 0
    && parts[0] !== 10
    && parts[0] !== 127
    && !(parts[0] === 100 && parts[1] >= 64 && parts[1] <= 127)
    && !(parts[0] === 169 && parts[1] === 254)
    && !(parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31)
    && !(parts[0] === 192 && parts[1] === 168)
    && parts[0] < 224;
}

function localPrivateAddresses() {
  return [...new Set(Object.values(os.networkInterfaces()).flatMap((rows) => (rows || [])
    .filter((row) => row.family === 'IPv4' && !row.internal && privateOverlayIpv4(row.address))
    .map((row) => row.address)))];
}

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 probeTcp(address, port, timeoutMs = 1500) {
  if (!address || !port) return Promise.resolve(false);
  return new Promise((resolve) => {
    const socket = net.createConnection({ host: address, port });
    let settled = false;
    const finish = (value) => {
      if (settled) return;
      settled = true;
      socket.destroy();
      resolve(value);
    };
    socket.setTimeout(timeoutMs);
    socket.once('connect', () => finish(true));
    socket.once('timeout', () => finish(false));
    socket.once('error', () => finish(false));
  });
}

async function main() {
  const topology = JSON.parse(fs.readFileSync(topologyPath, 'utf8'));
  let publicBridge = null;
  try {
    const config = JSON.parse(fs.readFileSync(publicBridgeConfigPath, 'utf8'));
    const publicKey = fs.readFileSync(publicBridgeKeyPath, 'utf8').trim().replace(/^0x/, '');
    const port = Number(config.p2pPort || 0);
    if (
      config.schemaVersion === 'plais-qbft-public-bridge/v1'
      && config.enabled === true
      && publicIpv4(config.advertisedHost)
      && Number.isInteger(port)
      && port >= 1024
      && port <= 65535
      && /^[a-f0-9]{128}$/i.test(publicKey)
    ) {
      const enode = `enode://${publicKey}@${config.advertisedHost}:${port}`;
      publicBridge = {
        host: config.advertisedHost,
        port,
        enodeHash: crypto.createHash('sha256').update(enode).digest('hex'),
        listening: await probeTcp('127.0.0.1', port),
        synced: false,
        syncAttestedAt: null
      };
      try {
        const attestation = JSON.parse(fs.readFileSync(publicBridgeSyncAttestationPath, 'utf8'));
        const observedAt = new Date(attestation.observedAt || 0).getTime();
        const ageMs = Date.now() - observedAt;
        const evidence = attestation.evidence || {};
        const bindings = attestation.bindings || {};
        const bridge = attestation.bridge || {};
        const latestBlockObservedAt = new Date(evidence.latestBlockObservedAt || 0).getTime();
        const headAgeMs = Date.now() - latestBlockObservedAt;
        publicBridge.synced = attestation.schemaVersion === 'plais-qbft-public-bridge-sync-attestation/v1'
          && attestation.status === 'SYNCED_AND_FOLLOWING_HEAD'
          && Number(attestation.chainId) === 20260718
          && bridge.host === publicBridge.host
          && Number(bridge.port) === publicBridge.port
          && bridge.validatorEnabled === false
          && bridge.rpcEnabled === false
          && bindings.configSha256 === sha256File(publicBridgeConfigPath)
          && bindings.publicKeySha256 === sha256File(publicBridgeKeyPath)
          && Number.isFinite(observedAt)
          && ageMs >= -5 * 60 * 1000
          && ageMs <= 24 * 60 * 60 * 1000
          && evidence.syncCompletionLog === 'Node is in sync, enabling transaction handling'
          && evidence.liveHeadFollowing === true
          && Number(evidence.peerCount || 0) >= minPublicBridgePeers
          && Number.isFinite(latestBlockObservedAt)
          && headAgeMs >= -60 * 1000
          && headAgeMs <= 5 * 60 * 1000
          && Number(evidence.canonicalBlock) >= Number(evidence.bridgeBlock)
          && Number(evidence.observedGapBlocks ?? Number.MAX_SAFE_INTEGER) <= 10;
        publicBridge.syncAttestedAt = publicBridge.synced ? attestation.observedAt : null;
      } catch (_) {}
    }
  } catch (error) {
    publicBridge = null;
  }
  const state = persistentStore.readState();
  const now = Date.now();
  const rows = await Promise.all(topology.validators.map(async (target) => {
    const node = latestNodeForTarget(state.nodes || [], target);
    const reportedAddresses = Array.isArray(node?.metadata?.privateNetworkAddresses)
      ? node.metadata.privateNetworkAddresses
      : [];
    const addresses = [...new Set(reportedAddresses.filter(privateOverlayIpv4))];
    const gatewayPort = Number(node?.metadata?.networkGatewayPort || 0);
    const publicEgressAddress = publicIpv4(node?.metadata?.publicEgressAddress)
      ? node.metadata.publicEgressAddress
      : '';
    const gatewayReachability = {};
    for (const address of addresses) gatewayReachability[address] = await probeTcp(address, gatewayPort);
    const heartbeatAgeSec = node?.lastHeartbeatAt
      ? Math.max(0, Math.round((now - new Date(node.lastHeartbeatAt).getTime()) / 1000))
      : null;
    const transportCheckedAt = node?.metadata?.chainTransportCheckedAt
      ? new Date(node.metadata.chainTransportCheckedAt).getTime()
      : Number.NaN;
    const transportAgeSec = Number.isFinite(transportCheckedAt)
      ? Math.max(0, Math.round((now - transportCheckedAt) / 1000))
      : null;
    const outboundBridgeVerified = Boolean(
      publicBridge
      && node?.metadata?.chainTransportSchema === 'plais-validator-transport-evidence/v1'
      && node?.metadata?.chainTransportMode === 'outbound-public-bridge'
      && node?.metadata?.chainTransportBridgeHost === publicBridge.host
      && Number(node?.metadata?.chainTransportBridgePort || 0) === publicBridge.port
      && node?.metadata?.chainTransportBridgeEnodeHash === publicBridge.enodeHash
      && node?.metadata?.chainTransportReachable === true
      && Number.isFinite(transportAgeSec)
      && transportAgeSec <= 300
    );
    const directTunnelVerified = Boolean(
      supportedTunnelClientVersions.has(node?.clientVersion)
      && node?.metadata?.validatorTunnelSchema === 'plais-validator-p2p-tunnel/v1'
      && ['direct-peers-applied', 'direct-lan-quorum-with-tunnel-fallback-applied']
        .includes(node?.metadata?.validatorTunnelState)
      && /^[a-f0-9]{64}$/.test(String(node?.metadata?.validatorTunnelTopologyHash || ''))
      && Number(node?.metadata?.validatorRuntimeProcessPid || 0) > 0
      && Number.isFinite(heartbeatAgeSec)
      && heartbeatAgeSec <= 300
    );
    return {
      nodeName: target.nodeName,
      pcName: target.pcName,
      active: node?.status === 'active' && Number.isFinite(heartbeatAgeSec) && heartbeatAgeSec <= 300,
      heartbeatAgeSec,
      reportedPrivateAddresses: addresses,
      observedPublicEgressAddress: publicEgressAddress,
      gatewayPort,
      gatewayReachability,
      outboundBridgeVerified,
      outboundBridgeEvidenceAgeSec: transportAgeSec,
      directTunnelVerified
    };
  }));
  const selected = rows.map((row) => row.reportedPrivateAddresses[0] || '').filter(Boolean);
  const uniqueTargets = new Set(selected);
  const publicEgressAddresses = rows.map((row) => row.observedPublicEgressAddress).filter(Boolean);
  const uniquePublicEgressAddresses = new Set(publicEgressAddresses);
  const bridgeAddresses = localPrivateAddresses();
  const checks = {
    bridgePrivateAddressPresent: bridgeAddresses.length > 0,
    allTargetsActive: rows.every((row) => row.active),
    allTargetsReported: rows.every((row) => row.reportedPrivateAddresses.length > 0),
    targetAddressesUnique: selected.length === rows.length && uniqueTargets.size === rows.length,
    allPublicEgressObserved: publicEgressAddresses.length === rows.length,
    publicEgressAddressesUnique: publicEgressAddresses.length === rows.length
      && uniquePublicEgressAddresses.size === rows.length,
    allReportedGatewaysReachable: rows.every((row) => row.reportedPrivateAddresses.some((address) => row.gatewayReachability[address] === true))
  };
  const privateRouteReady = checks.bridgePrivateAddressPresent
    && checks.allTargetsActive
    && checks.allTargetsReported
    && checks.targetAddressesUnique
    && checks.allReportedGatewaysReachable;
  const publicAddressPlanningReady = checks.allTargetsActive
    && checks.allPublicEgressObserved
    && checks.publicEgressAddressesUnique;
  checks.publicBridgeListening = publicBridge?.listening === true;
  checks.publicBridgeSynced = publicBridge?.synced === true;
  checks.allTargetsOutboundBridgeVerified = rows.every((row) => row.outboundBridgeVerified);
  checks.allTargetsDirectTunnelVerified = rows.every((row) => row.directTunnelVerified);
  const outboundBridgeReady = checks.allTargetsActive
    && checks.publicBridgeListening
    && checks.publicBridgeSynced
    && checks.allTargetsOutboundBridgeVerified;
  const directTunnelReady = checks.allTargetsActive && checks.allTargetsDirectTunnelVerified;
  const readyForTopology = privateRouteReady || outboundBridgeReady || directTunnelReady;
  console.log(JSON.stringify({
    ok: true,
    mode: 'signed-node-validator-network-inventory',
    readyForTopology,
    privateRouteReady,
    outboundBridgeReady,
    directTunnelReady,
    publicAddressPlanningReady,
    publicBridge: publicBridge ? {
      host: publicBridge.host,
      port: publicBridge.port,
      listening: publicBridge.listening,
      synced: publicBridge.synced,
      syncAttestedAt: publicBridge.syncAttestedAt
    } : null,
    bridgePrivateAddresses: bridgeAddresses,
    checks,
    rows,
    checkedAt: new Date().toISOString()
  }, null, 2));
}

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