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

const fs = require('fs');
const path = require('path');
const persistentStore = require('../services/persistentStore');
const { nodeOwner } = require('../services/validatorTargetBindingService');
const { parseDevicePublicKey } = require('../lib/validatorKeyEnvelope');
const { restrictAcl } = require('../lib/operatorDpapi');

const projectRoot = path.join(__dirname, '..');
const migrationRoot = path.join(projectRoot, 'runtime', 'plais-qbft-canonical-migration');
const topologyPath = path.join(migrationRoot, 'topology.json');
const envelopeRoot = path.join(migrationRoot, 'encrypted-key-envelopes');
const securityArchiveRoot = path.join(migrationRoot, 'security-artifact-archive');
const keyAttestationPath = path.join(projectRoot, 'runtime', 'mainnet-key-security-attestation.json');
const confirmPhrase = 'REPLACE_VALIDATOR_HOST_BINDING';
const supportedClientVersions = new Set([
  '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.107-quick-tunnel-discovery-default-off-locked',
  '1.4.109-runtime-heartbeat-null-stale-fallback-default-off-locked',
  '1.4.110-data-replica-readiness-attestation-default-off-locked'
]);

function parseArguments() {
  return Object.fromEntries(process.argv.slice(2).map((value) => {
    const match = String(value).match(/^--([a-z-]+)=(.+)$/i);
    return match ? [match[1], match[2]] : ['', ''];
  }).filter(([key]) => key));
}

function normalize(value) {
  return String(value || '').trim().toLowerCase();
}

function assertInside(parent, candidate) {
  const relative = path.relative(path.resolve(parent), path.resolve(candidate));
  if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
    throw new Error(`UNSAFE_REPLACEMENT_PATH:${candidate}`);
  }
}

function writeTemporaryTopology(filePath, value) {
  fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', flag: 'wx' });
  JSON.parse(fs.readFileSync(filePath, 'utf8'));
}

function latestReplacementNode(pcName, owner) {
  return (persistentStore.readState().nodes || [])
    .filter((node) => normalize(node.pcName || node.metadata?.hostname) === normalize(pcName)
      && nodeOwner(node) === normalize(owner))
    .sort((left, right) => new Date(right.lastHeartbeatAt || 0) - new Date(left.lastHeartbeatAt || 0))[0] || null;
}

function main() {
  const args = parseArguments();
  if (args.confirm !== confirmPhrase) throw new Error('EXACT_REPLACEMENT_CONFIRMATION_REQUIRED');
  if (!/^validator-[1-4]$/.test(String(args.node || ''))) throw new Error('EXACTLY_ONE_VALIDATOR_TARGET_REQUIRED');
  if (!/^[A-Za-z0-9._-]{1,64}$/.test(String(args['from-pc'] || ''))
    || !/^[A-Za-z0-9._-]{1,64}$/.test(String(args['to-pc'] || ''))
    || !/^[A-Za-z0-9._-]{1,64}$/.test(String(args.owner || ''))) {
    throw new Error('VALID_REPLACEMENT_IDENTITY_REQUIRED');
  }
  if (!/^\d{8}T\d{4}Z$/.test(String(args['archive-id'] || ''))) {
    throw new Error('SAFE_ARCHIVE_ID_REQUIRED');
  }

  const activationApproval = path.join(migrationRoot, 'validator-activation-approval.json');
  const tunnelApproval = path.join(migrationRoot, 'validator-tunnel-migration-approval.json');
  if (fs.existsSync(activationApproval) || fs.existsSync(tunnelApproval)) {
    throw new Error('VALIDATOR_ACTIVATION_OR_RESTART_APPROVAL_MUST_NOT_EXIST');
  }
  if (!fs.existsSync(topologyPath) || !fs.existsSync(envelopeRoot)) {
    throw new Error('LIVE_TOPOLOGY_OR_ENVELOPES_MISSING');
  }

  const topology = JSON.parse(fs.readFileSync(topologyPath, 'utf8'));
  if (topology.mode !== 'preserve-canonical-genesis'
    || Number(topology.chainId) !== 20260718
    || !Array.isArray(topology.validators)
    || topology.validators.length !== 4) {
    throw new Error('CANONICAL_FOUR_VALIDATOR_TOPOLOGY_INVALID');
  }
  const target = topology.validators.find((row) => row.nodeName === args.node);
  if (!target
    || normalize(target.pcName) !== normalize(args['from-pc'])
    || normalize(target.owner) !== normalize(args.owner)) {
    throw new Error('CURRENT_VALIDATOR_TARGET_BINDING_CHANGED');
  }
  if (topology.validators.some((row) => row.nodeName !== target.nodeName
    && normalize(row.pcName) === normalize(args['to-pc']))) {
    throw new Error('REPLACEMENT_PC_ALREADY_ASSIGNED');
  }

  const replacement = latestReplacementNode(args['to-pc'], args.owner);
  const metadata = replacement?.metadata || {};
  const heartbeatAt = new Date(replacement?.lastHeartbeatAt || 0).getTime();
  const heartbeatAgeMs = Date.now() - heartbeatAt;
  if (!replacement
    || replacement.status !== 'active'
    || !Number.isFinite(heartbeatAt)
    || heartbeatAgeMs < -60_000
    || heartbeatAgeMs > 3 * 60 * 1000
    || !supportedClientVersions.has(replacement.clientVersion)
    || metadata.provisioningKeyAlgorithm !== 'rsa-oaep-sha256-dpapi-current-user-v1'
    || !/^[a-f0-9]{64}$/.test(String(metadata.provisioningKeyId || ''))
    || !metadata.provisioningPublicKey) {
    throw new Error('REPLACEMENT_PC_NOT_READY');
  }
  parseDevicePublicKey(metadata.provisioningPublicKey, metadata.provisioningKeyId);

  const archiveRoot = path.join(securityArchiveRoot, args['archive-id']);
  const archivedTopology = path.join(archiveRoot, 'topology.before-validator-host-replacement.json');
  const archivedEnvelopeRoot = path.join(archiveRoot, 'encrypted-key-envelopes');
  const temporaryTopology = path.join(migrationRoot, `topology.replacement-${process.pid}-${Date.now()}.tmp`);
  for (const candidate of [archiveRoot, archivedTopology, archivedEnvelopeRoot, temporaryTopology]) {
    assertInside(migrationRoot, candidate);
  }
  if (fs.existsSync(archiveRoot) || fs.existsSync(temporaryTopology)) {
    throw new Error('REPLACEMENT_ARCHIVE_OR_TEMP_ALREADY_EXISTS');
  }

  const replacedAt = new Date().toISOString();
  const nextTopology = JSON.parse(JSON.stringify(topology));
  const nextTarget = nextTopology.validators.find((row) => row.nodeName === args.node);
  nextTarget.pcName = args['to-pc'];
  nextTarget.owner = args.owner;
  nextTarget.targetHost = '';
  nextTopology.updatedAt = replacedAt;
  nextTopology.hostReplacements = Array.isArray(nextTopology.hostReplacements)
    ? nextTopology.hostReplacements
    : [];
  nextTopology.hostReplacements.push({
    nodeName: args.node,
    fromPcName: target.pcName,
    toPcName: args['to-pc'],
    owner: args.owner,
    reason: 'operator-approved-hardware-repair-replacement',
    replacedAt
  });
  if (args['dry-run'] === 'true') {
    console.log(JSON.stringify({
      ok: true,
      state: 'VALIDATOR_HOST_BINDING_REPLACEMENT_DRY_RUN_READY',
      nodeName: args.node,
      fromPcName: target.pcName,
      toPcName: args['to-pc'],
      owner: args.owner,
      replacementClientVersion: replacement.clientVersion,
      replacementHeartbeatAgeSec: Math.max(0, Math.round(heartbeatAgeMs / 1000)),
      activationApprovalAbsent: true,
      tunnelRestartApprovalAbsent: true,
      liveFilesChanged: false
    }, null, 2));
    return;
  }
  writeTemporaryTopology(temporaryTopology, nextTopology);

  const artifacts = [
    path.join(migrationRoot, 'encrypted-key-delivery-approval.json'),
    path.join(migrationRoot, 'encrypted-key-export-approval.json'),
    path.join(migrationRoot, 'validator-runtime-deployment-approval.json'),
    path.join(migrationRoot, 'validator-pm2-state-attestation.json'),
    keyAttestationPath
  ].filter((filePath) => fs.existsSync(filePath));
  const moves = [];
  try {
    fs.mkdirSync(archiveRoot, { recursive: false });
    if (!restrictAcl(archiveRoot, true).ok) throw new Error('REPLACEMENT_ARCHIVE_ACL_FAILED');

    fs.renameSync(topologyPath, archivedTopology);
    moves.push([archivedTopology, topologyPath]);
    fs.renameSync(envelopeRoot, archivedEnvelopeRoot);
    moves.push([archivedEnvelopeRoot, envelopeRoot]);
    for (const source of artifacts) {
      const destination = path.join(archiveRoot, path.basename(source));
      if (fs.existsSync(destination)) throw new Error(`ARCHIVED_ARTIFACT_EXISTS:${path.basename(source)}`);
      fs.renameSync(source, destination);
      moves.push([destination, source]);
    }
    fs.renameSync(temporaryTopology, topologyPath);
    if (!restrictAcl(archiveRoot, true).ok) throw new Error('FINAL_REPLACEMENT_ARCHIVE_ACL_FAILED');
  } catch (error) {
    if (fs.existsSync(topologyPath) && !fs.existsSync(temporaryTopology)) {
      fs.renameSync(topologyPath, temporaryTopology);
    }
    for (const [archived, original] of moves.reverse()) {
      if (fs.existsSync(archived) && !fs.existsSync(original)) fs.renameSync(archived, original);
    }
    if (fs.existsSync(temporaryTopology)) fs.unlinkSync(temporaryTopology);
    throw error;
  }

  console.log(JSON.stringify({
    ok: true,
    state: 'VALIDATOR_HOST_BINDING_REPLACED_ENVELOPES_ARCHIVED',
    nodeName: args.node,
    fromPcName: target.pcName,
    toPcName: args['to-pc'],
    owner: args.owner,
    replacementClientVersion: replacement.clientVersion,
    replacementHeartbeatAgeSec: Math.max(0, Math.round(heartbeatAgeMs / 1000)),
    oldEncryptedEnvelopeSetPreserved: true,
    standingKeyDeliveryApproval: false,
    standingRuntimeApproval: false,
    keyDecryptImportStartAllowed: false,
    archiveRoot
  }, null, 2));
}

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