#!/usr/bin/env node
'use strict';
/**
 * verify-clue-fingerprints.js
 * Verifies that clue_fingerprint evidence in quest files is detectable
 * in the actual VM state.
 *
 * Usage (run from project root):
 *   node tools/content/verify-clue-fingerprints.js
 *   node tools/content/verify-clue-fingerprints.js --quest Q002
 *   node tools/content/verify-clue-fingerprints.js --revert
 *   node tools/content/verify-clue-fingerprints.js --start-vms
 *   node tools/content/verify-clue-fingerprints.js --dry-run
 *
 * Flags:
 *   --quest <ID>   Check only the specified quest (e.g. Q002)
 *   --revert       Revert each VM to its quest baseline snapshot before checking.
 *                  Leaves VMs running at baseline afterwards (ready for play).
 *   --start-vms    Start VMs that are not running (without reverting snapshot).
 *                  Use after a fresh boot if VMs are just stopped, not reverted.
 *   --dry-run      Print the SSH commands that would run without executing them.
 *   --help         Show this help text.
 *
 * Exit codes:
 *   0  All evidence confirmed (or dry-run)
 *   1  One or more failures, or no checks ran
 *
 * Prerequisites:
 *   - virsh available and pointing at qemu:///system
 *   - ~/.ssh/sc_host_key present
 *   - VMs running (or --revert / --start-vms provided)
 */

const fs      = require('fs');
const path    = require('path');
const { spawnSync } = require('child_process');
const os      = require('os');

// ---------------------------------------------------------------------------
// CONFIG
// ---------------------------------------------------------------------------

const PROJECT_ROOT    = path.resolve(__dirname, '../..');
const CONTENT_ROOT    = path.join(PROJECT_ROOT, 'content');
const QUESTS_DIR      = path.join(CONTENT_ROOT, 'quests');
const VM_PROFILES_DIR = path.join(CONTENT_ROOT, 'vm_profiles');
const SSH_KEY         = path.join(os.homedir(), '.ssh/sc_host_key');
const LIBVIRT_URI     = 'qemu:///system';
const VM_PREFIX       = 'sc-';
const SSH_WAIT_SECS   = 90;   // max seconds to wait for SSH after revert/start
const SSH_POLL_SECS   = 5;

const SSH_OPTS = [
  '-o', 'StrictHostKeyChecking=no',
  '-o', 'UserKnownHostsFile=/dev/null',
  '-o', 'BatchMode=yes',
  '-o', 'ConnectTimeout=8',
  '-o', 'LogLevel=ERROR',
  '-i', SSH_KEY,
];

// ---------------------------------------------------------------------------
// ARGS
// ---------------------------------------------------------------------------

const rawArgs    = process.argv.slice(2);
const dryRun     = rawArgs.includes('--dry-run');
const doRevert   = rawArgs.includes('--revert');
const startVms   = rawArgs.includes('--start-vms');
const showHelp   = rawArgs.includes('--help') || rawArgs.includes('-h');

const questIdx   = rawArgs.indexOf('--quest');
const questFilter = questIdx >= 0 ? rawArgs[questIdx + 1] : null;

if (showHelp) {
  const usage = fs.readFileSync(__filename, 'utf8').split('\n')
    .slice(1, 20).map(l => l.replace(/^ \* ?/, '')).join('\n');
  console.log(usage);
  process.exit(0);
}

// ---------------------------------------------------------------------------
// VIRSH HELPERS
// ---------------------------------------------------------------------------

function virsh(...args) {
  const r = spawnSync('virsh', ['--connect', LIBVIRT_URI, ...args], {
    encoding: 'utf8', timeout: 20000,
  });
  return {
    ok:     r.status === 0,
    stdout: (r.stdout || '').trim(),
    stderr: (r.stderr || '').trim(),
  };
}

function domainName(vmId) {
  return VM_PREFIX + vmId.replace(/_/g, '-');
}

function vmIsRunning(vmId) {
  const r = virsh('domstate', domainName(vmId));
  return r.ok && r.stdout === 'running';
}

function getVmIp(vmId) {
  const domain = domainName(vmId);
  // Primary: guest agent
  const r = virsh('domifaddr', domain, '--source', 'agent');
  if (r.ok && r.stdout) {
    const m = r.stdout.match(/(\d+\.\d+\.\d+\.\d+)/);
    if (m) return m[1];
  }
  // Fallback: DHCP leases table
  const leases = virsh('net-dhcp-leases', 'sc-internal');
  if (leases.ok && leases.stdout) {
    const hostname = domain.replace(VM_PREFIX, '');
    for (const line of leases.stdout.split('\n')) {
      if (line.includes(hostname)) {
        const m = line.match(/(\d+\.\d+\.\d+\.\d+)/);
        if (m) return m[1];
      }
    }
  }
  return null;
}

function sleep(secs) {
  spawnSync('sleep', [String(secs)]);
}

function waitForSsh(vmId, user) {
  process.stdout.write(`  Waiting for SSH on ${domainName(vmId)}`);
  const deadline = Date.now() + SSH_WAIT_SECS * 1000;
  while (Date.now() < deadline) {
    const ip = getVmIp(vmId);
    if (ip) {
      const r = spawnSync('ssh', [...SSH_OPTS, '-o', 'ConnectTimeout=5', `${user}@${ip}`, 'true'], {
        encoding: 'utf8', timeout: 8000,
      });
      if (r.status === 0) {
        console.log(` ready (${ip})`);
        return ip;
      }
    }
    process.stdout.write('.');
    sleep(SSH_POLL_SECS);
  }
  console.log(' TIMEOUT');
  return null;
}

function revertVmToBaseline(vmId, snapshot) {
  const domain = domainName(vmId);
  console.log(`  Reverting ${domain} → ${snapshot}...`);
  // Destroy (force-stop) if running — state will be discarded
  if (vmIsRunning(vmId)) {
    virsh('destroy', domain);
    sleep(1);
  }
  const r = virsh('snapshot-revert', domain, snapshot, '--running');
  if (!r.ok) {
    console.error(`  ERROR reverting ${domain}: ${r.stderr}`);
    return false;
  }
  return true;
}

// ---------------------------------------------------------------------------
// CONTENT LOADING
// ---------------------------------------------------------------------------

function loadVmProfiles() {
  const profiles = {};
  for (const f of fs.readdirSync(VM_PROFILES_DIR)) {
    if (!f.endsWith('.json')) continue;
    const d = JSON.parse(fs.readFileSync(path.join(VM_PROFILES_DIR, f), 'utf8'));
    profiles[d.id] = d;
  }
  return profiles;
}

function loadQuests() {
  return fs.readdirSync(QUESTS_DIR)
    .filter(f => f.endsWith('.json') && !f.includes('SPLIT'))
    .map(f => {
      try { return JSON.parse(fs.readFileSync(path.join(QUESTS_DIR, f), 'utf8')); }
      catch { return null; }
    })
    .filter(Boolean)
    .sort((a, b) => (a.id < b.id ? -1 : 1));
}

// ---------------------------------------------------------------------------
// EVIDENCE CHECK COMMAND BUILDER
// ---------------------------------------------------------------------------

function sq(s) {
  // Shell-quote a value for embedding in a bash command string
  return "'" + String(s).replace(/'/g, "'\"'\"'") + "'";
}

function buildCmd(ev) {
  // Returns a bash one-liner that prints "PASS" or "FAIL [detail]"
  // All commands run via sudo to handle restricted paths.
  switch (ev.type) {

    case 'file_absent':
      return `sudo test ! -e ${sq(ev.path)} && echo PASS || echo 'FAIL (file exists)'`;

    case 'file_exists':
      return `sudo test -e ${sq(ev.path)} && echo PASS || echo 'FAIL (file missing)'`;

    case 'file_contains':
    case 'log_contains': {
      const pat = sq(ev.contains);
      return `sudo grep -qF ${pat} ${sq(ev.path)} 2>/dev/null && echo PASS || echo 'FAIL (pattern not found)'`;
    }

    case 'file_owner_is_not': {
      const notExpected = ev.expected_user || ev.expected_owner || '';
      return (
        `owner=$(sudo stat -c '%U' ${sq(ev.path)} 2>/dev/null || echo '?'); ` +
        `[ "\${owner}" != ${sq(notExpected)} ] && echo PASS || ` +
        `echo "FAIL (owner=\${owner}, expected NOT ${notExpected})"`
      );
    }

    case 'file_size_above': {
      const threshold = ev.threshold_bytes;
      return (
        `bytes=$(sudo stat -c '%s' ${sq(ev.path)} 2>/dev/null || echo 0); ` +
        `[ "\${bytes}" -gt ${threshold} ] && echo PASS || ` +
        `echo "FAIL (size=\${bytes} bytes, threshold=${threshold})"`
      );
    }

    case 'disk_usage_above': {
      const threshold = ev.threshold_percent;
      return (
        `pct=$(sudo df --output=pcent ${sq(ev.path)} 2>/dev/null | tail -1 | tr -d ' %'); ` +
        `[ "\${pct}" -gt ${threshold} ] && echo PASS || ` +
        `echo "FAIL (usage=\${pct}%, threshold=${threshold}%)"`
      );
    }

    case 'service_state_is': {
      const expected = ev.state;
      return (
        `state=$(sudo systemctl is-active ${sq(ev.service)} 2>/dev/null || echo inactive); ` +
        `[ "\${state}" = ${sq(expected)} ] && echo PASS || ` +
        `echo "FAIL (state=\${state}, expected=${expected})"`
      );
    }

    case 'service_enabled_is': {
      const wantEnabled = ev.enabled === true || ev.enabled === 'true';
      return (
        `estate=$(sudo systemctl is-enabled ${sq(ev.service)} 2>/dev/null || echo disabled); ` +
        (wantEnabled
          ? `[ "\${estate}" = "enabled" ] && echo PASS || echo "FAIL (is-enabled=\${estate}, expected=enabled)"`
          : `[ "\${estate}" != "enabled" ] && echo PASS || echo "FAIL (is-enabled=\${estate}, expected=disabled)"`)
      );
    }

    default:
      return null;
  }
}

function describeEvidence(ev) {
  switch (ev.type) {
    case 'file_absent':        return `file absent: ${ev.path}`;
    case 'file_exists':        return `file exists: ${ev.path}`;
    case 'file_contains':
    case 'log_contains':       return `${ev.path} ∋ ${JSON.stringify(ev.contains)}`;
    case 'file_owner_is_not':  return `${ev.path} owner ≠ ${ev.expected_user || ev.expected_owner}`;
    case 'file_size_above':    return `${ev.path} > ${(ev.threshold_bytes / 1e9).toFixed(1)} GB`;
    case 'disk_usage_above':   return `${ev.path} disk > ${ev.threshold_percent}%`;
    case 'service_state_is':   return `${ev.service} state = ${ev.state}`;
    case 'service_enabled_is': return `${ev.service} enabled = ${ev.enabled}`;
    default:                   return `${ev.type}: ${JSON.stringify(ev)}`;
  }
}

// ---------------------------------------------------------------------------
// MAIN
// ---------------------------------------------------------------------------

function main() {
  console.log('\n  Sysadmin Chronicles — Clue Fingerprint Verifier');
  console.log('═══════════════════════════════════════\n');

  if (dryRun)   console.log('  Mode: DRY-RUN (no SSH commands executed)\n');
  if (doRevert) console.log('  Mode: REVERT — VMs will be reverted to baseline snapshots\n');

  const vmProfiles = loadVmProfiles();
  let quests = loadQuests();

  if (questFilter) {
    quests = quests.filter(q => q.id === questFilter);
    if (quests.length === 0) {
      console.error(`No quest found with id: ${questFilter}`);
      process.exit(1);
    }
  }

  // Collect VMs needed (evidence items might reference multiple VMs, e.g. Q008)
  const vmsNeeded = new Set();
  for (const q of quests) {
    for (const ev of (q.clue_fingerprint?.evidence || [])) {
      if (ev.vm) vmsNeeded.add(ev.vm);
    }
  }

  // Resolve IPs for non-revert mode upfront (revert mode resolves per-quest)
  const vmIps = {};
  if (!doRevert) {
    for (const vmId of vmsNeeded) {
      const domain = domainName(vmId);
      const profile = vmProfiles[vmId];
      const user = profile?.management_user || profile?.ssh_user || 'player';

      if (!vmIsRunning(vmId)) {
        if (startVms) {
          console.log(`Starting ${domain}...`);
          virsh('start', domain);
          vmIps[vmId] = waitForSsh(vmId, user);
        } else {
          console.warn(`  ⚠  ${domain} is not running. Use --start-vms or --revert.`);
          vmIps[vmId] = null;
        }
      } else {
        process.stdout.write(`  Resolving IP for ${domain}...`);
        let ip = null;
        const deadline = Date.now() + 30000;
        while (!ip && Date.now() < deadline) {
          ip = getVmIp(vmId);
          if (!ip) { process.stdout.write('.'); sleep(3); }
        }
        console.log(ip ? ` ${ip}` : ' (could not resolve)');
        vmIps[vmId] = ip;
      }
    }
  }

  // ---------------------------------------------------------------------------
  // Per-quest checks
  // ---------------------------------------------------------------------------

  let totalPass = 0, totalFail = 0, totalSkip = 0;
  const failures = [];

  for (const quest of quests) {
    const evidence = quest.clue_fingerprint?.evidence || [];
    if (evidence.length === 0) {
      console.log(`\n[${quest.id}] ${quest.title} — no evidence entries, skipping`);
      continue;
    }

    const baseline = quest.baseline_snapshot || '(unknown)';
    const primaryVm = quest.primary_vm || '';
    console.log(`\n[${quest.id}] ${quest.title}`);
    console.log(`  Baseline: ${primaryVm} @ ${baseline}`);

    // Revert primary VM if --revert
    if (doRevert && primaryVm && !dryRun) {
      const revOk = revertVmToBaseline(primaryVm, baseline);
      if (!revOk) {
        console.log(`  ✗  Cannot revert — skipping ${evidence.length} checks`);
        totalSkip += evidence.length;
        continue;
      }
      const profile = vmProfiles[primaryVm];
      const user = profile?.management_user || profile?.ssh_user || 'player';
      const ip = waitForSsh(primaryVm, user);
      vmIps[primaryVm] = ip;
      if (!ip) {
        console.log(`  ✗  SSH timeout after revert — skipping ${evidence.length} checks`);
        totalSkip += evidence.length;
        continue;
      }
    }

    // Also handle multi-VM quests (Q008 references both web_server and build_machine)
    const questVms = new Set(evidence.map(ev => ev.vm).filter(Boolean));
    if (doRevert) {
      for (const extraVm of questVms) {
        if (extraVm === primaryVm) continue;
        if (!vmIps[extraVm]) {
          // Extra VM: just ensure it's running (don't revert — only primary gets reverted)
          if (!vmIsRunning(extraVm)) {
            virsh('start', domainName(extraVm));
          }
          const profile = vmProfiles[extraVm];
          const user = profile?.management_user || profile?.ssh_user || 'player';
          vmIps[extraVm] = waitForSsh(extraVm, user);
        }
      }
    }

    // Run each evidence check
    for (const ev of evidence) {
      const vmId = ev.vm || primaryVm;
      const ip   = vmIps[vmId];
      const profile = vmProfiles[vmId];
      const user = profile?.management_user || profile?.ssh_user || 'player';
      const desc = describeEvidence(ev);

      if (!ip && !dryRun) {
        console.log(`  ⚠  SKIP  ${desc}`);
        console.log(`         → VM ${vmId} not reachable`);
        totalSkip++;
        continue;
      }

      const cmd = buildCmd(ev);
      if (!cmd) {
        console.log(`  ⚠  SKIP  ${desc}`);
        console.log(`         → evidence type '${ev.type}' not supported by this tool`);
        totalSkip++;
        continue;
      }

      let passed;
      let detail = '';

      if (dryRun) {
        console.log(`  ○  DRY   ${desc}`);
        console.log(`         → ssh ${user}@<${vmId}-ip> '${cmd.slice(0, 80)}${cmd.length > 80 ? '...' : ''}'`);
        totalPass++;
        continue;
      }

      const r = spawnSync('ssh', [...SSH_OPTS, `${user}@${ip}`, cmd], {
        encoding: 'utf8', timeout: 25000,
      });
      const stdout = (r.stdout || '').trim();
      const stderr = (r.stderr || '').trim();

      if (r.error || r.status === null) {
        passed = false;
        detail = r.error ? r.error.message : 'SSH process failed';
      } else {
        passed = stdout.startsWith('PASS');
        detail = stdout.startsWith('FAIL') ? stdout.replace(/^FAIL\s*/, '') : (stderr || `exit ${r.status}`);
      }

      if (passed) {
        console.log(`  ✓  PASS  ${desc}`);
        totalPass++;
      } else {
        console.log(`  ✗  FAIL  ${desc}`);
        if (detail) console.log(`         → ${detail}`);
        totalFail++;
        failures.push({ quest: quest.id, desc, detail });
      }
    }
  }

  // ---------------------------------------------------------------------------
  // Summary
  // ---------------------------------------------------------------------------

  console.log('\n═══════════════════════════════════════');
  console.log(`  Results: ${totalPass} passed  ${totalFail} failed  ${totalSkip} skipped`);

  if (totalFail > 0) {
    console.log('\n  Failed checks:');
    for (const f of failures) {
      console.log(`  ✗ [${f.quest}] ${f.desc}`);
      if (f.detail) console.log(`    → ${f.detail}`);
    }
    console.log('\n  ✗ Clue fingerprint verification FAILED');
    console.log('═══════════════════════════════════════\n');
    process.exit(1);
  }

  if (!dryRun && totalPass === 0 && totalSkip > 0) {
    console.log('\n  ⚠  No checks ran. Are the VMs running?');
    console.log('  Tips:');
    console.log('    --start-vms   start VMs then check on current state');
    console.log('    --revert      revert to baseline snapshot then check');
    console.log('═══════════════════════════════════════\n');
    process.exit(1);
  }

  if (dryRun) {
    console.log('\n  Dry-run complete. No checks were executed.');
  } else {
    console.log('\n  ✓ All evidence confirmed in VM state');
  }
  console.log('═══════════════════════════════════════\n');
  process.exit(0);
}

main();
