const fs = require('fs');
const path = require('path');

const projectRoot = path.join(__dirname, '..');
const ledgerPath = path.join(projectRoot, 'data', 'ledger.json');
const defaultExclusionPath = 'E:\\aisfor\\runtime\\sync-client-canary-exclusions.json';
const rolePlanPath = path.join(projectRoot, 'config', 'resilienceNodeRoles.example.json');
const fallbackCandidateNames = ['ellia05', 'Ellis-test-main'];
const freshLimitMs = 5 * 60 * 1000;

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

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

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

function pcName(node = {}) {
  return String(node.pcName || node.metadata?.hostname || '').trim();
}

function resolveCandidateNames() {
  try {
    const rolePlan = readJson(rolePlanPath, null);
    if (!rolePlan || !Array.isArray(rolePlan.nodes)) {
      return fallbackCandidateNames;
    }
    const planned = rolePlan.nodes
      .filter((entry) => Array.isArray(entry.candidateRoles) && entry.candidateRoles.length > 0)
      .map((entry) => String(entry.node || '').trim())
      .filter(Boolean)
      .filter((value, index, list) => list.indexOf(value) === index);
    return planned.length > 0 ? planned : fallbackCandidateNames;
  } catch (_) {
    return fallbackCandidateNames;
  }
}

function latestByPc(nodes, expectedName) {
  return nodes
    .filter((node) => normalize(pcName(node)) === normalize(expectedName))
    .sort((left, right) => new Date(right.lastHeartbeatAt || 0) - new Date(left.lastHeartbeatAt || 0))[0] || null;
}

function gib(value) {
  const numeric = Number(value);
  return Number.isFinite(numeric) ? Math.round((numeric / (1024 ** 3)) * 100) / 100 : null;
}

function quotaGiB(node = {}) {
  const metadata = node.metadata || {};
  if (Number.isFinite(Number(metadata.storageQuotaGiB))) return Number(metadata.storageQuotaGiB);
  if (Number.isFinite(Number(node.storageQuotaGiB))) return Number(node.storageQuotaGiB);
  return gib(metadata.storageQuotaBytes ?? node.storageQuotaBytes ?? node.storageLimitBytes);
}

function usedGiB(node = {}) {
  const metadata = node.metadata || {};
  if (Number.isFinite(Number(metadata.storageUsedGiB))) return Number(metadata.storageUsedGiB);
  if (Number.isFinite(Number(node.storageUsedGiB))) return Number(node.storageUsedGiB);
  return gib(metadata.storageUsedBytes ?? node.storageUsedBytes ?? node.usedStorageBytes);
}

function main() {
  const state = readJson(ledgerPath);
  if (!state || !Array.isArray(state.nodes)) throw new Error('Local ledger node inventory is unavailable');
  const exclusionPath = process.argv.find((value) => value.startsWith('--exclusions='))?.slice('--exclusions='.length)
    || defaultExclusionPath;
  const exclusion = readJson(exclusionPath, {});
  const rolePlan = readJson(rolePlanPath, {});
  const candidateNames = resolveCandidateNames();
  const excludedPcNames = new Set((exclusion.pcNames || []).map(normalize));
  const excludedDeviceIds = new Set((exclusion.deviceIds || []).map(String));
  const now = Date.now();

  const rows = candidateNames.map((expectedName) => {
    const node = latestByPc(state.nodes, expectedName);
    if (!node) {
      return {
        pcName: expectedName,
        found: false,
        eligibleForRolePreparation: false,
        blockers: ['NODE_REGISTRATION_NOT_FOUND']
      };
    }
    const metadata = node.metadata || {};
    const roleEntry = (rolePlan.nodes || []).find((item) => normalize(item.node) === normalize(expectedName)) || {};
    const operatorAssertions = roleEntry.operatorAssertions || {};
    const heartbeatAtMs = new Date(node.lastHeartbeatAt || 0).getTime();
    const heartbeatAgeSec = Number.isFinite(heartbeatAtMs) ? Math.max(0, Math.round((now - heartbeatAtMs) / 1000)) : null;
    const heartbeatFresh = node.status === 'active'
      && heartbeatAgeSec !== null
      && heartbeatAgeSec <= freshLimitMs / 1000;
    const excludedByExistingPolicy = exclusion.status === 'ACTIVE'
      && (excludedPcNames.has(normalize(expectedName)) || excludedDeviceIds.has(deviceIdentity(node)));
    const validatorState = String(metadata.validatorRuntimeState || '');
    const validatorAssigned = metadata.chainProvisioningRole === 'validator-candidate'
      || /^validator-/i.test(validatorState)
      || validatorState === 'presync-ready';
    const inventoryCheckedAtMs = metadata.resilienceInventoryCheckedAt
      ? new Date(metadata.resilienceInventoryCheckedAt).getTime()
      : Number.NaN;
    const inventoryAgeSec = Number.isFinite(inventoryCheckedAtMs)
      ? Math.max(0, Math.round((now - inventoryCheckedAtMs) / 1000))
      : null;
    const authenticatedInventoryReady = metadata.resilienceInventorySchema === 'plai-resilience-node-inventory/v1'
      && /^[a-f0-9]{64}$/.test(String(metadata.resilienceInventoryCollectorSha256 || ''))
      && inventoryAgeSec !== null && inventoryAgeSec <= freshLimitMs / 1000
      && Number(metadata.resilienceLogicalCpuCount || 0) >= 1
      && Number(metadata.resilienceTotalMemoryBytes || 0) >= 256 * 1024 * 1024
      && Number(metadata.resilienceNodeVolumeTotalBytes || 0) > 0
      && Number(metadata.resilienceNodeVolumeFreeBytes || 0) >= 0
      && metadata.resilienceNodeVolumeStatus === 'CAPACITY_READ_ONLY'
      && metadata.resilienceStorageHealth === 'UNKNOWN_NON_ADMIN';
    const blockers = [];
    if (!heartbeatFresh) blockers.push('HEARTBEAT_NOT_FRESH');
    if (excludedByExistingPolicy) blockers.push('EXCLUDED_BY_EXISTING_OPERATOR_POLICY');
    if (validatorAssigned) blockers.push('VALIDATOR_ROLE_SEPARATION_REQUIRED');
    if (!authenticatedInventoryReady) blockers.push('AUTHENTICATED_READONLY_HARDWARE_INVENTORY_MISSING');
    if (operatorAssertions.powerLossAutoBootAvailable === false) {
      blockers.push('AUTOBOOT_NOT_AVAILABLE_MANUAL_RECOVERY_ONLY');
    } else if (operatorAssertions.powerLossAutoBootAvailable !== true) {
      blockers.push('AUTOBOOT_UNVERIFIED');
    }
    if (operatorAssertions.upsPresent !== true) blockers.push('UPS_UNVERIFIED');
    blockers.push('SITE_POWER_ISP_INDEPENDENCE_UNPROVEN');

    return {
      pcName: expectedName,
      found: true,
      status: String(node.status || ''),
      lastHeartbeatAt: node.lastHeartbeatAt || null,
      heartbeatAgeSec,
      heartbeatFresh,
      clientVersion: String(node.clientVersion || ''),
      storageQuotaGiB: quotaGiB(node),
      storageUsedGiB: usedGiB(node),
      chainProvisioningRole: metadata.chainProvisioningRole || null,
      chainProvisioningState: metadata.chainProvisioningState || null,
      validatorRuntimeState: metadata.validatorRuntimeState || null,
      validatorTunnelState: metadata.validatorTunnelState || null,
      excludedByExistingPolicy,
      operatorAssertions: {
        powerLossAutoBootAvailable: operatorAssertions.powerLossAutoBootAvailable ?? null,
        recoveryModeAfterPowerLoss: operatorAssertions.recoveryModeAfterPowerLoss || null,
        upsPresent: operatorAssertions.upsPresent ?? null,
        siteIndependenceVerified: operatorAssertions.siteIndependenceVerified ?? null,
        ispIndependenceVerified: operatorAssertions.ispIndependenceVerified ?? null,
        assertedAt: operatorAssertions.assertedAt || null
      },
      authenticatedInventory: {
        ready: authenticatedInventoryReady,
        checkedAt: metadata.resilienceInventoryCheckedAt || null,
        ageSec: inventoryAgeSec,
        collectorSha256: metadata.resilienceInventoryCollectorSha256 || null,
        logicalCpuCount: Number(metadata.resilienceLogicalCpuCount || 0),
        totalMemoryGiB: gib(metadata.resilienceTotalMemoryBytes),
        freeMemoryGiB: gib(metadata.resilienceFreeMemoryBytes),
        hostUptimeSeconds: Number(metadata.resilienceHostUptimeSeconds || 0),
        nodeVolumeRoot: metadata.resilienceNodeVolumeRoot || null,
        nodeVolumeTotalGiB: gib(metadata.resilienceNodeVolumeTotalBytes),
        nodeVolumeFreeGiB: gib(metadata.resilienceNodeVolumeFreeBytes),
        nodeVolumeStatus: metadata.resilienceNodeVolumeStatus || null,
        storageHealth: metadata.resilienceStorageHealth || null
      },
      eligibleForRolePreparation: blockers.length === 0,
      blockers
    };
  });

  console.log(JSON.stringify({
    ok: true,
    mode: 'READ_ONLY_LOCAL_LEDGER_CANDIDATE_READINESS',
    rows,
    safety: {
      environmentRead: false,
      databaseConnected: false,
      networkRequestsMade: false,
      filesWritten: false,
      remoteCommandsExecuted: false,
      roleActivated: false
    },
    checkedAt: new Date().toISOString()
  }, null, 2));
}

try {
  main();
} catch (error) {
  console.error(JSON.stringify({ ok: false, mode: 'READ_ONLY_LOCAL_LEDGER_CANDIDATE_READINESS', error: error.message }, null, 2));
  process.exitCode = 1;
}
