require('../config/loadEnv')();
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
const { getAddress, isAddress, parseEther } = require('ethers');
const { EVM_CANDIDATE_CONFIG } = require('../config/evmCandidateConfig');

const projectRoot = path.join(__dirname, '..');
const runtimeRoot = path.join(projectRoot, 'runtime');
const besuRoot = path.join(runtimeRoot, 'besu-26.6.1');
const javaHome = path.join(runtimeRoot, 'jdk-25.0.3+9');
const javaCommand = path.join(javaHome, 'bin', 'java.exe');
const besuClasspath = path.join(besuRoot, 'lib', '*');
const planRoot = path.join(runtimeRoot, 'plais-qbft-distributed');
const generatedRoot = path.join(planRoot, 'generated');
const topologySource = path.join(projectRoot, 'config', 'qbftDistributedTopology.example.json');
const topologyPath = path.join(planRoot, 'topology.json');

function requirePath(filePath, label) {
  if (!fs.existsSync(filePath)) throw new Error(`${label} is missing: ${filePath}`);
}

function readOperatorAddress() {
  const address = String(process.env.PLAIS_EVM_OPERATOR_ADDRESS || '').trim();
  if (!isAddress(address)) throw new Error('PLAIS_EVM_OPERATOR_ADDRESS is required.');
  return getAddress(address);
}

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

function main() {
  if (process.env.PLAIS_QBFT_ALLOW_NEW_GENESIS !== '1') {
    throw new Error('Blocked: this command creates a different genesis. Use evm:init-canonical-migration to preserve the canonical chain.');
  }
  requirePath(path.join(besuRoot, 'lib'), 'Besu');
  requirePath(javaCommand, 'Temurin JDK');
  requirePath(topologySource, 'Distributed topology template');
  if (fs.existsSync(path.join(planRoot, 'network.json'))) {
    throw new Error('Distributed QBFT keys already exist; existing validator keys were left untouched.');
  }

  const topology = JSON.parse(fs.readFileSync(topologySource, 'utf8'));
  if (!Array.isArray(topology.validators) || topology.validators.length !== 5) {
    throw new Error('Distributed topology must contain exactly five physical-PC validators.');
  }
  const uniquePcNames = new Set(topology.validators.map((node) => String(node.pcName).toLowerCase()));
  if (uniquePcNames.size !== topology.validators.length) {
    throw new Error('Only one validator may be assigned to each physical PC.');
  }

  const operatorAddress = readOperatorAddress();
  fs.mkdirSync(planRoot, { recursive: true });
  const configPath = path.join(planRoot, 'qbftConfigFile.json');
  const config = {
    genesis: {
      config: {
        chainId: Number(topology.chainId),
        berlinBlock: 0,
        londonBlock: 0,
        zeroBaseFee: true,
        qbft: { blockperiodseconds: 2, epochlength: 30000, requesttimeoutseconds: 4 }
      },
      nonce: '0x0',
      timestamp: '0x0',
      gasLimit: '0x1c9c380',
      difficulty: '0x1',
      mixHash: '0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365',
      coinbase: '0x0000000000000000000000000000000000000000',
      alloc: {
        [operatorAddress.slice(2).toLowerCase()]: {
          balance: `0x${parseEther(EVM_CANDIDATE_CONFIG.genesisSupply).toString(16)}`
        }
      }
    },
    blockchain: { nodes: { generate: true, count: topology.validators.length } }
  };
  fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);

  const result = spawnSync(javaCommand, [
    '--enable-native-access=ALL-UNNAMED', '-Xms128m', '-Xmx512m',
    '-cp', besuClasspath, 'org.hyperledger.besu.Besu',
    'operator', 'generate-blockchain-config',
    `--config-file=${configPath}`, `--to=${generatedRoot}`, '--private-key-file-name=key'
  ], {
    cwd: planRoot,
    env: { ...process.env, JAVA_HOME: javaHome },
    encoding: 'utf8',
    shell: false
  });
  if (result.status !== 0) throw new Error(result.stderr || `Besu exited with ${result.status}`);

  const keyRoot = path.join(generatedRoot, 'keys');
  const keyDirs = fs.readdirSync(keyRoot, { withFileTypes: true })
    .filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
  if (keyDirs.length !== topology.validators.length) throw new Error('Unexpected validator key count.');

  const packageRoot = path.join(planRoot, 'packages');
  const nodes = topology.validators.map((entry, index) => {
    const keyDir = keyDirs[index];
    const packageDir = path.join(packageRoot, entry.name);
    fs.mkdirSync(packageDir, { recursive: true });
    copyFile(path.join(generatedRoot, 'genesis.json'), path.join(packageDir, 'genesis.json'));
    copyFile(path.join(keyRoot, keyDir, 'key'), path.join(packageDir, 'key'));
    copyFile(path.join(keyRoot, keyDir, 'key.pub'), path.join(packageDir, 'key.pub'));
    copyFile(path.join(projectRoot, 'deploy', 'qbft-validator', 'start-validator.ps1'), path.join(packageDir, 'start-validator.ps1'));
    copyFile(path.join(projectRoot, 'deploy', 'qbft-validator', 'install-validator.ps1'), path.join(packageDir, 'install-validator.ps1'));
    const node = {
      ...entry,
      p2pPort: Number(entry.p2pPort || topology.p2pPort || 30303),
      validatorAddress: keyDir,
      publicKey: fs.readFileSync(path.join(keyRoot, keyDir, 'key.pub'), 'utf8').trim().replace(/^0x/, ''),
      finalized: false
    };
    fs.writeFileSync(path.join(packageDir, 'node-config.json'), `${JSON.stringify(node, null, 2)}\n`);
    return node;
  });

  const plan = {
    version: 1,
    state: 'AWAITING_NETWORK_ADDRESSES',
    networkName: topology.networkName,
    chainId: Number(topology.chainId),
    consensus: 'QBFT',
    validatorCount: nodes.length,
    operatorAddress,
    nodes
  };
  const editableNodes = nodes.map(({ publicKey, validatorAddress, finalized, ...node }) => node);
  fs.writeFileSync(topologyPath, `${JSON.stringify({ ...topology, validators: editableNodes }, null, 2)}\n`);
  fs.writeFileSync(path.join(planRoot, 'network.json'), `${JSON.stringify(plan, null, 2)}\n`);
  console.log(JSON.stringify({ ok: true, state: plan.state, validators: nodes.length, topologyPath, packageRoot }));
}

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