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

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

const projectRoot = path.join(__dirname, '..');
const topologyPath = path.join(projectRoot, 'runtime', 'plais-qbft-canonical-migration', 'topology.json');
const requiredClientVersion = '1.4.25-independent-heartbeat-single-install-locked';
const acceptedValidatorClientVersions = 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.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'
]);
const aisforRoot = path.resolve(String(process.env.AISFOR_APP_ROOT || 'E:\\aisfor'));
const exclusionPath = path.join(aisforRoot, 'runtime', 'sync-client-canary-exclusions.json');
const candidateAttestationPath = path.join(aisforRoot, 'runtime', 'sync-client-candidate-integration-attestation.json');
const canaryResultPath = path.join(aisforRoot, 'runtime', 'sync-client-canary-last-result.json');
const rolloutPolicyPath = path.join(aisforRoot, 'runtime', 'sync-client-update-rollout.json');
const activeHeartbeatLimitMs = 5 * 60 * 1000;

function readJson(filePath, fallback = null) {
  try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch (_) { return fallback; }
}

function readFleetControls(now = Date.now()) {
  const exclusion = readJson(exclusionPath, {});
  const attestation = readJson(candidateAttestationPath, {});
  const canary = readJson(canaryResultPath, {});
  const attestedAt = new Date(attestation.verifiedAt || 0).getTime();
  const expiresAt = new Date(attestation.expiresAt || 0).getTime();
  const candidateVersion = String(attestation.candidate?.version || '');
  const candidateVerified = attestation.schemaVersion === 'aisfor-sync-client-candidate-integration-attestation/v1'
    && attestation.status === 'CANDIDATE_INTEGRATION_VERIFIED_NOT_DEPLOYED'
    && Number.isFinite(attestedAt)
    && attestedAt <= now
    && Number.isFinite(expiresAt)
    && expiresAt > now
    && Object.values(attestation.checks || {}).every(value => value === true)
    && canary.schemaVersion === 'aisfor-sync-client-canary-result/v2'
    && canary.ok === true
    && canary.status === 'CANARY_READY_HEARTBEAT_CONFIRMED'
    && canary.targetVersion === candidateVersion
    && canary.targetSha256 === attestation.candidate?.sha256
    && canary.policyFrozen === true
    && canary.rolloutPolicyPresent === false
    && !fs.existsSync(rolloutPolicyPath);
  const exclusionsValid = exclusion.schemaVersion === 'aisfor-sync-client-canary-exclusions/v1'
    && exclusion.status === 'ACTIVE';
  return {
    exclusionsValid,
    excludedDeviceIds: new Set(exclusionsValid ? (exclusion.deviceIds || []).map(String) : []),
    excludedPcNames: new Set(exclusionsValid
      ? (exclusion.pcNames || []).map(value => String(value).trim().toLowerCase())
      : []),
    candidateVerified,
    candidateVersion: candidateVerified ? candidateVersion : '',
    candidateSha256: candidateVerified ? String(attestation.candidate?.sha256 || '') : ''
  };
}

function deviceIdentity(node = {}) {
  return String(node.metadata?.syncDeviceId || node.metadata?.aisforNodeId || node.nodeId || node.id || '').trim();
}

function latestNodesByDevice(nodes = []) {
  const latest = new Map();
  for (const node of nodes) {
    const identity = deviceIdentity(node);
    if (!identity) continue;
    const previous = latest.get(identity);
    if (!previous || new Date(node.lastHeartbeatAt || 0) > new Date(previous.lastHeartbeatAt || 0)) {
      latest.set(identity, node);
    }
  }
  return [...latest.values()];
}

function provisioningKeyReady(metadata = {}) {
  return metadata.provisioningKeyAlgorithm === 'rsa-oaep-sha256-dpapi-current-user-v1'
    && /^[a-f0-9]{64}$/.test(String(metadata.provisioningKeyId || ''))
    && Boolean(metadata.provisioningPublicKey);
}

function main() {
  const topology = JSON.parse(fs.readFileSync(topologyPath, 'utf8'));
  const validatorNames = new Set((topology.validators || []).map((row) => String(row.pcName || '').trim().toLowerCase()));
  const now = Date.now();
  const controls = readFleetControls(now);
  const registered = latestNodesByDevice(persistentStore.readState().nodes || []);
  const allActive = registered.filter((node) => {
    const heartbeatAt = new Date(node.lastHeartbeatAt || 0).getTime();
    return node.status === 'active' && Number.isFinite(heartbeatAt) && now - heartbeatAt <= activeHeartbeatLimitMs;
  });
  const excludedActive = allActive.filter((node) => {
    const pcName = String(node.pcName || node.metadata?.hostname || '').trim().toLowerCase();
    return controls.excludedDeviceIds.has(deviceIdentity(node)) || controls.excludedPcNames.has(pcName);
  });
  const active = allActive.filter((node) => !excludedActive.includes(node));
  const rows = active.map((node) => {
    const pcName = String(node.pcName || node.metadata?.hostname || '').trim();
    const metadata = node.metadata || {};
    const heartbeatAgeSec = Math.max(0, Math.round((now - new Date(node.lastHeartbeatAt).getTime()) / 1000));
    const validatorTarget = findValidatorTarget(topology, node);
    const expectedRole = validatorTarget ? 'validator-candidate' : 'participant';
    const expectedState = expectedRole === 'validator-candidate' ? 'awaiting-operator-approval' : 'registered';
    const clientVersion = String(node.clientVersion || '');
    const checks = {
      clientReady: clientVersion === requiredClientVersion
        || (expectedRole === 'validator-candidate' && acceptedValidatorClientVersions.has(clientVersion))
        || (controls.candidateVerified && clientVersion === controls.candidateVersion),
      deviceEncryptionKeyReady: provisioningKeyReady(metadata),
      schemaVerified: metadata.chainProvisioningSchema === 'plais-node-provisioning/v1',
      receiptVerified: /^[a-f0-9]{64}$/.test(String(metadata.chainProvisioningHash || '').trim().toLowerCase()),
      roleVerified: metadata.chainProvisioningRole === expectedRole,
      stateVerified: metadata.chainProvisioningState === expectedState
    };
    return {
      pcName,
      role: expectedRole,
      clientVersion,
      heartbeatAgeSec,
      checks,
      ready: Object.values(checks).every(Boolean)
    };
  }).sort((left, right) => left.pcName.localeCompare(right.pcName));
  const validatorCandidates = rows.filter((row) => row.role === 'validator-candidate');
  const participants = rows.filter((row) => row.role === 'participant');
  const topologyCovered = validatorCandidates.length === validatorNames.size
    && [...validatorNames].every((name) => validatorCandidates.some((row) => row.pcName.toLowerCase() === name));
  const requiredParticipantCount = 0;
  const requiredActiveDeviceCount = validatorNames.size + requiredParticipantCount;
  const ready = rows.length >= requiredActiveDeviceCount
    && validatorCandidates.length === validatorNames.size
    && participants.length >= requiredParticipantCount
    && topologyCovered
    && validatorCandidates.every((row) => row.ready)
    && participants.filter((row) => row.ready).length >= requiredParticipantCount;
  console.log(JSON.stringify({
    ok: true,
    mode: 'active-sync-client-fleet-readiness',
    requiredClientVersion,
    acceptedCandidateVersion: controls.candidateVersion || null,
    candidateCanaryVerified: controls.candidateVerified,
    exclusionsValid: controls.exclusionsValid,
    excludedActiveDeviceCount: excludedActive.length,
    excludedActiveDevices: excludedActive.map(node => ({
      deviceId: deviceIdentity(node),
      pcName: String(node.pcName || node.metadata?.hostname || '').trim()
    })),
    requiredActiveDeviceCount,
    requiredParticipantCount,
    activeDeviceCount: rows.length,
    validatorCandidateCount: validatorCandidates.length,
    participantCount: participants.length,
    topologyValidatorCount: validatorNames.size,
    topologyCovered,
    ready,
    rows,
    checkedAt: new Date().toISOString()
  }, null, 2));
  if (!ready) process.exitCode = 2;
}

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