const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const { findValidatorTarget } = require('./validatorTargetBindingService');

const projectRoot = path.join(__dirname, '..');
const runtimeRoot = path.join(projectRoot, 'runtime');
const migrationRoot = path.join(runtimeRoot, 'plais-qbft-canonical-migration');
const topologyPath = path.join(migrationRoot, 'topology.json');
const genesisPath = path.join(runtimeRoot, 'plais-qbft', 'generated', 'genesis.json');
const runtimeManifestPath = path.join(runtimeRoot, 'plais-validator-runtime', 'manifest.json');
const bridgeConfigPath = path.join(runtimeRoot, 'plais-qbft-public-bridge', 'config.json');
const bridgePublicKeyPath = path.join(runtimeRoot, 'plais-qbft-public-bridge', 'data', 'key.pub');
const bridgeSyncAttestationPath = path.join(runtimeRoot, 'plais-qbft-public-bridge', 'sync-attestation.json');
const approvalPath = path.join(migrationRoot, 'validator-runtime-deployment-approval.json');

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

function readJson(filePath) {
  return JSON.parse(fs.readFileSync(filePath, 'utf8'));
}

function getBinding(node) {
  for (const required of [topologyPath, genesisPath, runtimeManifestPath, bridgeConfigPath, bridgePublicKeyPath]) {
    if (!fs.existsSync(required)) return null;
  }
  const topology = readJson(topologyPath);
  const runtime = readJson(runtimeManifestPath);
  const bridge = readJson(bridgeConfigPath);
  const pcName = String(node?.pcName || node?.metadata?.hostname || '').trim();
  const target = findValidatorTarget(topology, node);
  if (!target) return null;
  const deviceKeyId = String(node?.metadata?.provisioningKeyId || '').trim().toLowerCase();
  const publicKey = fs.readFileSync(bridgePublicKeyPath, 'utf8').trim().replace(/^0x/, '');
  const genesisSha256 = sha256File(genesisPath);
  const valid = topology.mode === 'preserve-canonical-genesis'
    && Number(topology.chainId) === 20260718
    && topology.genesisSha256 === genesisSha256
    && runtime.schemaVersion === 'plais-validator-runtime-artifact/v1'
    && runtime.contents?.privateKeysIncluded === false
    && runtime.contents?.chainConfigurationIncluded === false
    && /^[a-f0-9]{64}$/.test(String(runtime.sha256 || ''))
    && Number(runtime.bytes) > 100 * 1024 * 1024
    && String(runtime.publicPath || '').startsWith('/public/downloads/')
    && bridge.schemaVersion === 'plais-qbft-public-bridge/v1'
    && bridge.enabled === true
    && /^[a-f0-9]{128}$/i.test(publicKey)
    && /^[a-f0-9]{64}$/.test(deviceKeyId);
  if (!valid) return null;
  return { node, topology, runtime, bridge, target, pcName, deviceKeyId, publicKey, genesisSha256 };
}

function validateApproval(binding) {
  if (!fs.existsSync(approvalPath)) return { ok: false, reason: 'RUNTIME_DEPLOYMENT_APPROVAL_MISSING' };
  try {
    const approval = readJson(approvalPath);
    const now = Date.now();
    const approvedAt = new Date(approval.approvedAt || 0).getTime();
    const expiresAt = new Date(approval.expiresAt || 0).getTime();
    const expectedNodeNames = (binding.topology.validators || []).map((target) => target.nodeName).sort();
    const approvedNodeNames = Object.keys(approval.deviceKeyIds || {}).sort();
    const approvedDeviceKeyIds = Object.values(approval.deviceKeyIds || {}).map((value) => String(value || '').toLowerCase());
    const targetNodeNames = Array.isArray(approval.targetNodeNames) ? approval.targetNodeNames : [];
    const controls = approval.controls || {};
    const valid = approval.action === 'DEPLOY_VALIDATOR_RUNTIME_PRESYNC'
      && approval.schemaVersion === 'plais-validator-runtime-deployment-approval/v1'
      && approval.topologySha256 === sha256File(topologyPath)
      && approval.genesisSha256 === binding.genesisSha256
      && approval.runtimeManifestSha256 === sha256File(runtimeManifestPath)
      && approval.bridgeConfigSha256 === sha256File(bridgeConfigPath)
      && JSON.stringify(approvedNodeNames) === JSON.stringify(expectedNodeNames)
      && approvedDeviceKeyIds.length === 4
      && new Set(approvedDeviceKeyIds).size === 4
      && approvedDeviceKeyIds.every((value) => /^[a-f0-9]{64}$/.test(value))
      && targetNodeNames.length === 1
      && /^validator-[1-4]$/.test(String(targetNodeNames[0] || ''))
      && targetNodeNames.includes(binding.target.nodeName)
      && String(approval.deviceKeyIds?.[binding.target.nodeName] || '') === binding.deviceKeyId
      && controls.runtimeDownloadAllowed === true
      && controls.preSyncStartAllowed === true
      && controls.validatorKeyDecryptAllowed === false
      && controls.validatorKeyImportAllowed === false
      && controls.validatorStartAllowed === false
      && controls.arbitraryCommandAllowed === false
      && /^[A-Za-z0-9._:-]{8,128}$/.test(String(approval.approvalId || ''))
      && Number.isFinite(approvedAt)
      && approvedAt <= now
      && approvedAt >= now - 24 * 60 * 60 * 1000
      && Number.isFinite(expiresAt)
      && expiresAt > now
      && expiresAt <= approvedAt + 4 * 60 * 60 * 1000;
    return valid ? { ok: true, approval } : { ok: false, reason: 'RUNTIME_DEPLOYMENT_APPROVAL_INVALID' };
  } catch (_) {
    return { ok: false, reason: 'RUNTIME_DEPLOYMENT_APPROVAL_INVALID' };
  }
}

function buildOffer(node) {
  const binding = getBinding(node);
  if (!binding) return { ok: false, status: 404, reason: 'VALIDATOR_RUNTIME_TARGET_NOT_FOUND' };
  const approval = validateApproval(binding);
  const issuedAt = new Date();
  const bridgePort = Number(binding.bridge.p2pPort || 0);
  let canonicalBlock = 0;
  try {
    const attestation = readJson(bridgeSyncAttestationPath);
    const observedAt = new Date(attestation.observedAt || 0).getTime();
    if (
      attestation.schemaVersion === 'plais-qbft-public-bridge-sync-attestation/v1'
      && attestation.status === 'SYNCED_AND_FOLLOWING_HEAD'
      && Date.now() - observedAt >= -60_000
      && Date.now() - observedAt <= 24 * 60 * 60 * 1000
    ) canonicalBlock = Math.max(0, Number(attestation.evidence?.canonicalBlock || 0));
  } catch (_) {}
  const offer = {
    schemaVersion: 'plais-validator-runtime-deployment-offer/v1',
    nodeId: String(node.nodeId || node.id || ''),
    pcName: binding.pcName,
    nodeName: binding.target.nodeName,
    chainId: Number(binding.topology.chainId),
    canonicalBlock,
    genesisSha256: binding.genesisSha256,
    genesisFileBase64: fs.readFileSync(genesisPath).toString('base64'),
    deviceKeyId: binding.deviceKeyId,
    runtime: {
      version: binding.runtime.version,
      downloadPath: binding.runtime.publicPath,
      bytes: Number(binding.runtime.bytes),
      sha256: binding.runtime.sha256,
      besuDirectoryName: binding.runtime.contents.besuDirectoryName,
      jdkDirectoryName: binding.runtime.contents.jdkDirectoryName
    },
    transport: {
      mode: 'outbound-public-bridge',
      staticNodes: [`enode://${binding.publicKey}@${binding.bridge.advertisedHost}:${bridgePort}`],
      outboundOnly: true,
      inboundFirewallRuleRequired: false,
      p2pPort: 30303,
      localRpcHost: '127.0.0.1',
      localRpcPort: 18545
    },
    state: approval.ok ? 'approved-awaiting-runtime-presync' : 'locked-awaiting-runtime-approval',
    controls: {
      runtimeDownloadAllowed: approval.ok,
      preSyncStartAllowed: approval.ok,
      validatorKeyDecryptAllowed: false,
      validatorKeyImportAllowed: false,
      validatorStartAllowed: false,
      arbitraryCommandAllowed: false
    },
    approvalId: approval.ok ? String(approval.approval.approvalId || '') : '',
    issuedAt: issuedAt.toISOString(),
    expiresAt: new Date(issuedAt.getTime() + 15 * 60 * 1000).toISOString()
  };
  return { ok: true, offer };
}

module.exports = { buildOffer, paths: { approvalPath, runtimeManifestPath } };
