const crypto = require('crypto');
const fs = require('fs');
const net = require('net');
const path = require('path');
const { computeAddress, SigningKey } = require('ethers');

const projectRoot = path.join(__dirname, '..');
const activeRoot = path.join(projectRoot, 'runtime', 'plais-qbft');
const migrationRoot = path.join(projectRoot, 'runtime', 'plais-qbft-canonical-migration');
const topologyPath = path.join(migrationRoot, 'topology.json');
const planPath = path.join(migrationRoot, 'network.json');
const packagesRoot = path.join(migrationRoot, 'packages');
const bridgePath = path.join(migrationRoot, 'bridge-topology.json');
const deployRoot = path.join(projectRoot, 'deploy', 'qbft-canonical');

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

function normalizePublicKey(value) {
  return String(value || '').trim().toLowerCase().replace(/^0x/, '').replace(/^04/, '');
}

function isPrivateOverlayIp(value) {
  const host = String(value || '').trim();
  const version = net.isIP(host);
  if (version === 4) {
    const parts = host.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);
  }
  return false;
}

function readNodeIdentity(node) {
  const dataRoot = path.join(activeRoot, node.name, 'data');
  const publicKey = normalizePublicKey(fs.readFileSync(path.join(dataRoot, 'key.pub'), 'utf8'));
  return {
    ...node,
    publicKey,
    enodeId: publicKey
  };
}

function enode(publicKey, host, port) {
  return `enode://${normalizePublicKey(publicKey)}@${host}:${Number(port)}`;
}

function writeJson(filePath, value) {
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
  fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
}

function copyFile(source, destination) {
  fs.mkdirSync(path.dirname(destination), { recursive: true });
  fs.copyFileSync(source, destination);
}

function writeManifest(packageDir, metadata) {
  const files = fs.readdirSync(packageDir)
    .filter((name) => name !== 'manifest.json')
    .sort()
    .map((name) => ({ name, sha256: sha256File(path.join(packageDir, name)) }));
  writeJson(path.join(packageDir, 'manifest.json'), {
    version: 1,
    ...metadata,
    files,
    createdAt: new Date().toISOString()
  });
}

function validatePrivateIdentity(activeNode, topologyNode) {
  const keyPath = path.join(activeRoot, activeNode.name, 'data', 'key');
  const privateKey = fs.readFileSync(keyPath, 'utf8').trim();
  if (!/^(0x)?[0-9a-f]{64}$/i.test(privateKey)) throw new Error(`Invalid private key file for ${activeNode.name}.`);
  const signingKey = new SigningKey(privateKey.startsWith('0x') ? privateKey : `0x${privateKey}`);
  const publicKey = normalizePublicKey(signingKey.publicKey);
  const address = computeAddress(signingKey.publicKey).toLowerCase();
  if (publicKey !== normalizePublicKey(topologyNode.publicKey)
    || address !== String(topologyNode.validatorAddress).toLowerCase()
    || address !== String(activeNode.address).toLowerCase()) {
    throw new Error(`Canonical validator identity mismatch for ${activeNode.name}.`);
  }
  return { keyPath, publicKey, address };
}

function main() {
  if (!fs.existsSync(topologyPath)) throw new Error('Run npm run evm:init-canonical-migration first.');
  if (fs.existsSync(packagesRoot) || fs.existsSync(planPath)) {
    throw new Error('Canonical migration packages already exist; existing private-key packages were left untouched.');
  }
  for (const fileName of ['install-node.ps1', 'start-node.ps1', 'preflight-node.ps1', 'approve-node-activation.ps1']) {
    const scriptPath = path.join(deployRoot, fileName);
    if (!fs.existsSync(scriptPath)) throw new Error(`Deployment script is missing: ${scriptPath}`);
  }

  const network = JSON.parse(fs.readFileSync(path.join(activeRoot, 'network.json'), 'utf8'));
  const topology = JSON.parse(fs.readFileSync(topologyPath, 'utf8'));
  const genesisPath = path.join(activeRoot, 'generated', 'genesis.json');
  const genesisHash = sha256File(genesisPath);
  if (topology.mode !== 'preserve-canonical-genesis' || topology.state !== 'AWAITING_PRIVATE_NETWORK_ADDRESSES') {
    throw new Error('Topology is not an unfinalized canonical-genesis migration plan.');
  }
  if (Number(topology.chainId) !== Number(network.chainId) || topology.genesisSha256 !== genesisHash) {
    throw new Error('Canonical chain ID or genesis hash changed after migration initialization.');
  }
  if (!isPrivateOverlayIp(topology.bridge && topology.bridge.advertisedHost)) {
    throw new Error('Set bridge.advertisedHost to a reachable private LAN/VPN IP address.');
  }

  const activeNodes = (network.nodes || []).map(readNodeIdentity);
  const activeValidators = activeNodes.filter((node) => node.role === 'validator');
  const activeRpc = activeNodes.find((node) => node.role === 'rpc');
  if (activeValidators.length !== 4 || !activeRpc) throw new Error('Expected four active validators and one RPC node.');
  if (!Array.isArray(topology.validators) || topology.validators.length !== 4) {
    throw new Error('Exactly four canonical validator targets are required.');
  }

  const nodeNames = new Set();
  const pcNames = new Set();
  const targetHosts = new Set();
  for (const node of topology.validators) {
    if (nodeNames.has(node.nodeName)) throw new Error(`Duplicate nodeName: ${node.nodeName}`);
    if (pcNames.has(String(node.pcName).toLowerCase())) throw new Error(`Duplicate physical PC: ${node.pcName}`);
    if (!isPrivateOverlayIp(node.targetHost)) throw new Error(`Set a private LAN/VPN targetHost for ${node.nodeName}.`);
    if (targetHosts.has(String(node.targetHost).toLowerCase())) throw new Error('Each validator needs a unique target host.');
    if (!activeValidators.some((active) => active.name === node.nodeName)) throw new Error(`Unknown canonical validator: ${node.nodeName}`);
    if (!Number.isInteger(Number(node.p2pPort)) || Number(node.p2pPort) < 1024 || Number(node.p2pPort) > 65535) {
      throw new Error(`Invalid P2P port for ${node.nodeName}.`);
    }
    nodeNames.add(node.nodeName);
    pcNames.add(String(node.pcName).toLowerCase());
    targetHosts.add(String(node.targetHost).toLowerCase());
  }

  const bridgeHost = topology.bridge.advertisedHost;
  const targetByName = new Map(topology.validators.map((node) => [node.nodeName, node]));
  const allPeerHosts = [...new Set([bridgeHost, ...topology.validators.map((node) => node.targetHost)])];
  const bridgeNodes = activeNodes.map((node) => {
    const staticNodes = [];
    for (const peer of activeValidators) {
      if (peer.name === node.name) continue;
      const target = targetByName.get(peer.name);
      staticNodes.push(enode(peer.publicKey, bridgeHost, peer.p2pPort));
      staticNodes.push(enode(peer.publicKey, target.targetHost, target.p2pPort));
    }
    return { name: node.name, role: node.role, p2pPort: node.p2pPort, staticNodes: [...new Set(staticNodes)] };
  });
  writeJson(bridgePath, {
    version: 1,
    state: 'READY_FOR_BRIDGE',
    mode: topology.mode,
    chainId: network.chainId,
    genesisSha256: genesisHash,
    advertisedHost: bridgeHost,
    interface: topology.bridge.interface || '0.0.0.0',
    allowedRemoteHosts: [...targetHosts],
    nodes: bridgeNodes
  });

  const presyncStaticNodes = [
    enode(activeRpc.publicKey, bridgeHost, activeRpc.p2pPort),
    ...activeValidators.map((node) => enode(node.publicKey, bridgeHost, node.p2pPort))
  ];
  const packageScripts = ['install-node.ps1', 'start-node.ps1', 'preflight-node.ps1', 'approve-node-activation.ps1'];
  const publicPlanNodes = [];

  for (const topologyNode of topology.validators) {
    const activeNode = activeValidators.find((node) => node.name === topologyNode.nodeName);
    const identity = validatePrivateIdentity(activeNode, topologyNode);
    const peerEnodes = [];
    for (const peer of activeValidators) {
      if (peer.name === activeNode.name) continue;
      const target = targetByName.get(peer.name);
      peerEnodes.push(enode(peer.publicKey, bridgeHost, peer.p2pPort));
      peerEnodes.push(enode(peer.publicKey, target.targetHost, target.p2pPort));
    }

    for (const role of ['presync', 'validator']) {
      const packageDir = path.join(packagesRoot, role, activeNode.name);
      fs.mkdirSync(packageDir, { recursive: true });
      const nodeConfig = {
        version: 1,
        mode: topology.mode,
        finalized: true,
        role,
        nodeName: activeNode.name,
        pcName: topologyNode.pcName,
        owner: topologyNode.owner,
        chainId: Number(network.chainId),
        genesisSha256: genesisHash,
        validatorAddress: identity.address,
        publicKey: identity.publicKey,
        advertisedHost: topologyNode.targetHost,
        p2pPort: Number(topologyNode.p2pPort),
        allowedPeerHosts: allPeerHosts.filter((host) => host !== topologyNode.targetHost),
        activationRequired: role === 'validator'
      };
      copyFile(genesisPath, path.join(packageDir, 'genesis.json'));
      writeJson(path.join(packageDir, 'node-config.json'), nodeConfig);
      writeJson(path.join(packageDir, 'static-nodes.json'), role === 'presync' ? presyncStaticNodes : [...new Set(peerEnodes)]);
      copyFile(path.join(activeRoot, activeNode.name, 'data', 'key.pub'), path.join(packageDir, 'key.pub'));
      if (role === 'validator') {
        copyFile(identity.keyPath, path.join(packageDir, 'validator.key'));
        try { fs.chmodSync(path.join(packageDir, 'validator.key'), 0o600); } catch (error) { /* Installer enforces Windows ACLs. */ }
      }
      for (const fileName of packageScripts) copyFile(path.join(deployRoot, fileName), path.join(packageDir, fileName));
      writeManifest(packageDir, { role, nodeName: activeNode.name, genesisSha256: genesisHash });
    }
    publicPlanNodes.push({
      nodeName: activeNode.name,
      pcName: topologyNode.pcName,
      owner: topologyNode.owner,
      validatorAddress: identity.address,
      publicKey: identity.publicKey,
      currentHost: bridgeHost,
      currentP2pPort: activeNode.p2pPort,
      targetHost: topologyNode.targetHost,
      targetP2pPort: Number(topologyNode.p2pPort),
      state: 'PACKAGED_NOT_ACTIVATED'
    });
  }

  const plan = {
    version: 1,
    state: 'READY_FOR_PRESYNC',
    mode: topology.mode,
    networkName: topology.networkName,
    chainId: Number(network.chainId),
    consensus: 'QBFT',
    genesisSha256: genesisHash,
    bridgeHost,
    requiredIndependentHosts: 4,
    activationPolicy: 'stop-local-identity-before-remote-activation',
    nodes: publicPlanNodes,
    finalizedAt: new Date().toISOString()
  };
  writeJson(planPath, plan);
  writeJson(topologyPath, { ...topology, state: plan.state, finalizedAt: plan.finalizedAt });
  console.log(JSON.stringify({
    ok: true,
    state: plan.state,
    mode: plan.mode,
    chainId: plan.chainId,
    validatorCount: plan.nodes.length,
    bridgePath,
    packagesRoot
  }, null, 2));
}

try { main(); } catch (error) { console.error(error.message); process.exit(1); }
