#!/usr/bin/env node
/*
 * scripts/doctor.ts — single-command health diagnostic for a PF forge
 *
 * Designed for the moment a teammate (or future-you, on a different
 * machine) types "is anything wrong" and wants a single command to
 * answer it. Runs 8 checks in order, with each check's status surfaced
 * inline and a numbered summary at the end.
 *
 * Exit code mirrors the worst severity observed:
 *   0 = everything green (or only "info" warnings)
 *   1 = at least one ⚠ warning (degraded but functional)
 *   2 = at least one ✗ failure (forge is broken somewhere)
 *
 * Run from the repo root:
 *   npx tsx scripts/doctor.ts
 *
 * No external deps beyond what PF already uses; works without the
 * MCP client being loaded (everything goes through REST or the
 * filesystem).
 */

import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';

type Severity = 'ok' | 'warn' | 'fail' | 'info';

interface CheckResult {
  name: string;
  severity: Severity;
  detail: string;
  hint?: string;
}

const results: CheckResult[] = [];

function record(name: string, severity: Severity, detail: string, hint?: string): void {
  results.push({ name, severity, detail, hint });
  const icon =
    severity === 'ok' ? '\x1b[32m✓\x1b[0m' :
    severity === 'warn' ? '\x1b[33m⚠\x1b[0m' :
    severity === 'fail' ? '\x1b[31m✗\x1b[0m' :
    '\x1b[36mℹ\x1b[0m';
  console.log(`${icon} ${name}: ${detail}`);
  if (hint && severity !== 'ok') console.log(`    → ${hint}`);
}

async function fetchJson(
  url: string,
  timeoutMs = 3000,
): Promise<{ ok: boolean; status: number; body: unknown; err?: string }> {
  try {
    const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
    const text = await res.text();
    let body: unknown = text;
    try { body = JSON.parse(text); } catch { /* leave as string */ }
    return { ok: res.ok, status: res.status, body };
  } catch (err) {
    return { ok: false, status: 0, body: null, err: err instanceof Error ? err.message : String(err) };
  }
}

interface HubStatus {
  enabled?: boolean;
  memory?: {
    enabled?: boolean;
    backend?: string;
    teamId?: string;
    agentId?: string;
  };
  pulse?: { emit?: boolean };
  orchestration?: { enabled?: boolean };
  warnings?: string[];
  health?: { ok?: boolean; backend?: string; detail?: string };
  pgvectorConnectionStringSet?: boolean;
  pgvectorAgentTokenSet?: boolean;
  pgvectorCredsComplete?: boolean;
  xmemCredsComplete?: boolean;
  forceNoopEnv?: boolean;
  configPath?: string;
}

async function main(): Promise<void> {
  console.log('═══════════════════════════════════════════════════════════════');
  console.log(' PF DOCTOR — diagnostic snapshot');
  console.log('═══════════════════════════════════════════════════════════════');
  console.log('');

  // ── 1. Backend reachable? ──────────────────────────────────────────
  const health = await fetchJson('http://127.0.0.1:3456/health');
  if (health.ok) {
    const body = health.body as { status?: string; version?: string };
    record('Backend reachable', 'ok', `HTTP 200 — ${body.version ?? 'unknown version'}`);
  } else {
    record(
      'Backend reachable',
      'fail',
      `cannot reach http://127.0.0.1:3456/health (${health.err ?? 'HTTP ' + health.status})`,
      'Start the backend: `systemctl --user start priority-forge-backend` (Linux) or `./setup.sh start`',
    );
    // No point running the rest of the network-dependent checks
    printSummaryAndExit();
    return;
  }

  // ── 2. Hub backend + identity ──────────────────────────────────────
  const status = await fetchJson('http://127.0.0.1:3456/xmem/status');
  if (!status.ok) {
    record('Hub status', 'fail', `endpoint failed: ${status.err ?? 'HTTP ' + status.status}`);
  } else {
    const s = status.body as HubStatus;
    const memEnabled = s.memory?.enabled ?? s.enabled;
    const backend = s.memory?.backend ?? s.health?.backend;
    const teamId = s.memory?.teamId;
    const agentId = s.memory?.agentId;
    const teamHubPath = path.join(os.homedir(), '.priority-forge', 'team-hub.json');
    if (fs.existsSync(teamHubPath)) {
      record('team-hub.json', 'ok', teamHubPath);
    } else {
      record(
        'team-hub.json',
        'info',
        'not found (using defaults until hub_configure)',
        'Run hub_configure or xmem_enable_sync to create ~/.priority-forge/team-hub.json',
      );
    }
    if (!memEnabled) {
      record(
        'Hub enabled',
        'warn',
        'memory.enabled=false',
        'Run `hub_configure({ memory: { enabled: true, backend, teamId, agentId } })`',
      );
    } else {
      record('Hub enabled', 'ok', `backend=${backend}, teamId=${teamId}, agentId=${agentId}`);
    }
    if (memEnabled && s.pulse?.emit === false) {
      record(
        'pulse.emit',
        'warn',
        'memory on but pulse.emit=false — events may not reach hub',
        'hub_configure auto-enables pulse when enabling memory; set pulse.emit true',
      );
    } else if (s.pulse?.emit) {
      record('pulse.emit', 'ok', 'true');
    }
    if (Array.isArray(s.warnings) && s.warnings.length > 0) {
      for (const w of s.warnings) {
        record('Hub warning', 'warn', w);
      }
    }

    if (s.forceNoopEnv) {
      record(
        'XMEM_FORCE_NOOP env',
        'warn',
        'set in env — hub forced to noop regardless of config',
        'Unset XMEM_FORCE_NOOP and restart the backend if this is unexpected',
      );
    }

    // ── 3. Hub health probe ─────────────────────────────────────────
    if (s.health?.ok) {
      record('Hub health', 'ok', `${s.health.backend}: ${s.health.detail ?? 'reachable'}`);
    } else if (memEnabled) {
      record(
        'Hub health',
        'fail',
        `${s.health?.backend ?? 'unknown'}: ${s.health?.detail ?? 'health probe failed'}`,
        s.health?.detail?.includes('connection failed')
          ? "Your IP may not be in the RDS security group. Get IP with `curl checkip.amazonaws.com` and send to the operator."
          : s.health?.detail?.includes('table')
            ? 'Schemas may not be applied. Re-run scripts/bootstrap-hub.ts with --skip-smoke.'
            : 'Inspect ~/.priority-forge/team-hub.json for misconfiguration.',
      );
    } else {
      record('Hub health', 'info', 'skipped (hub not enabled)');
    }

    // ── 4. Credential completeness ──────────────────────────────────
    if (backend === 'pgvector') {
      if (s.pgvectorCredsComplete) {
        record('pgvector creds', 'ok', 'connectionString + agentToken both set');
      } else {
        const missing: string[] = [];
        if (!s.pgvectorConnectionStringSet) missing.push('connectionString');
        if (!s.pgvectorAgentTokenSet) missing.push('agentToken');
        record(
          'pgvector creds',
          'warn',
          `missing: ${missing.join(', ')}`,
          'Run `hub_configure({ memory: { backend: "pgvector", pgvector: { connectionString, agentToken } } })` or paste the configSnippet from team-tokens-*.json into ~/.priority-forge/team-hub.json (memory.pgvector.{connectionString,agentToken})',
        );
      }
    } else if (backend === 'xmem') {
      if (s.xmemCredsComplete) {
        record('xmem creds', 'ok', 'apiKey + orgID both set');
      } else {
        record('xmem creds', 'warn', 'apiKey or orgID missing', 'Set via xmem_enable_sync');
      }
    } else {
      record(`${backend ?? 'unknown'} creds`, 'info', 'no creds required for this backend');
    }
  }

  // ── 5. Config file existence + perms ───────────────────────────────
  // team-hub.json is the canonical config (contains pgvector.agentToken +
  // xmem.apiKey — both 0600-grade secrets). Legacy xmem.json gets a soft
  // cleanup hint if it lingers after migration completed.
  const teamHubCfg = path.join(os.homedir(), '.priority-forge', 'team-hub.json');
  const legacyXmemCfg = path.join(os.homedir(), '.priority-forge', 'xmem.json');
  if (fs.existsSync(teamHubCfg)) {
    const stat = fs.statSync(teamHubCfg);
    const mode = (stat.mode & 0o777).toString(8);
    if (mode === '600') {
      record('team-hub.json perms', 'ok', `mode 0600 at ${teamHubCfg}`);
    } else {
      record(
        'team-hub.json perms',
        'warn',
        `mode 0${mode} at ${teamHubCfg} (contains agentToken + apiKey — should be 0600)`,
        `\`chmod 600 ${teamHubCfg}\``,
      );
    }
  } else {
    record(
      'team-hub.json',
      'warn',
      `${teamHubCfg} does not exist`,
      'Run `hub_configure({ memory: { enabled: true, backend: "dryrun", teamId, agentId } })` to create one',
    );
  }
  if (fs.existsSync(legacyXmemCfg) && fs.existsSync(teamHubCfg)) {
    record(
      'legacy xmem.json',
      'info',
      `${legacyXmemCfg} still present after team-hub.json migration`,
      `Safe to remove: \`rm ${legacyXmemCfg}\` (and team-pulse.json if present)`,
    );
  }

  // ── 6. systemd service status (Linux only) ─────────────────────────
  if (process.platform === 'linux') {
    try {
      const { execSync } = await import('child_process');
      const out = execSync('systemctl --user is-active priority-forge-backend 2>&1', {
        encoding: 'utf8',
      }).trim();
      if (out === 'active') {
        record('systemd service', 'ok', 'priority-forge-backend is active');
      } else {
        record(
          'systemd service',
          'warn',
          `is-active returned: ${out}`,
          '`systemctl --user status priority-forge-backend` for diagnostic',
        );
      }
    } catch (err) {
      record(
        'systemd service',
        'warn',
        `cannot query systemd: ${err instanceof Error ? err.message : err}`,
        'You may be running PF via `./setup.sh start` (nohup) instead of systemd — that\'s fine; this check just doesn\'t apply.',
      );
    }
  } else {
    record('systemd service', 'info', 'skipped (non-Linux platform)');
  }

  // ── 7. MCP registration ────────────────────────────────────────────
  // Heuristic: check that ~/.claude.json has a priority-forge entry for
  // the current cwd's project mapping. This is the path Claude Code
  // actually uses (per priority-forge-protocol.md).
  const claudeJsonPath = path.join(os.homedir(), '.claude.json');
  if (fs.existsSync(claudeJsonPath)) {
    try {
      const claudeCfg = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf8')) as {
        projects?: Record<string, { mcpServers?: Record<string, unknown> }>;
      };
      const cwd = process.cwd();
      const projectEntry = claudeCfg.projects?.[cwd];
      if (projectEntry?.mcpServers?.['priority-forge']) {
        record('Claude Code MCP', 'ok', `priority-forge registered for ${cwd}`);
      } else {
        record(
          'Claude Code MCP',
          'warn',
          `priority-forge NOT registered for ${cwd}`,
          `From this directory: \`claude mcp add --transport http --scope local priority-forge http://127.0.0.1:3456/mcp\``,
        );
      }
    } catch (err) {
      record(
        'Claude Code MCP',
        'warn',
        `could not parse ~/.claude.json: ${err instanceof Error ? err.message : err}`,
      );
    }
  } else {
    record('Claude Code MCP', 'info', '~/.claude.json not found (Claude Code not installed?)');
  }

  // ── 8. Cross-forge state (only if hub backend is enabled + healthy) ─
  if (status.ok) {
    const s = status.body as HubStatus;
    const memEnabled = s.memory?.enabled ?? s.enabled;
    if (memEnabled && s.health?.ok) {
      const myClaims = await fetchJson('http://127.0.0.1:3456/shared-tasks/my-claims');
      if (myClaims.ok) {
        const body = myClaims.body as { sharedTasks?: unknown[] };
        const n = body.sharedTasks?.length ?? 0;
        record('Active claims (mine)', n > 0 ? 'info' : 'ok', `${n} SharedTask(s) currently held`);
      }
      const claimable = await fetchJson('http://127.0.0.1:3456/shared-tasks/list?status=claimable');
      if (claimable.ok) {
        const body = claimable.body as { sharedTasks?: unknown[] };
        const n = body.sharedTasks?.length ?? 0;
        record('Claimable inbox', n > 0 ? 'info' : 'ok', `${n} SharedTask(s) available to claim`);
      }
    } else {
      record('Cross-forge state', 'info', 'skipped (hub not active)');
    }
  }

  printSummaryAndExit();
}

function printSummaryAndExit(): never {
  console.log('');
  console.log('═══════════════════════════════════════════════════════════════');
  console.log(' SUMMARY');
  console.log('═══════════════════════════════════════════════════════════════');
  const counts = { ok: 0, warn: 0, fail: 0, info: 0 };
  for (const r of results) counts[r.severity]++;
  console.log(`  ✓ ${counts.ok}   ⚠ ${counts.warn}   ✗ ${counts.fail}   ℹ ${counts.info}`);
  let exitCode = 0;
  if (counts.fail > 0) {
    console.log('');
    console.log(' Critical failures — forge is broken:');
    for (const r of results) {
      if (r.severity === 'fail') console.log(`   ✗ ${r.name}: ${r.detail}`);
    }
    exitCode = 2;
  } else if (counts.warn > 0) {
    console.log('');
    console.log(' Warnings — degraded but functional:');
    for (const r of results) {
      if (r.severity === 'warn') console.log(`   ⚠ ${r.name}: ${r.detail}`);
    }
    exitCode = 1;
  } else {
    console.log('');
    console.log(' All systems nominal. Forge is healthy.');
  }
  console.log('═══════════════════════════════════════════════════════════════');
  process.exit(exitCode);
}

main().catch((err) => {
  console.error('[doctor] unexpected error:', err instanceof Error ? err.message : err);
  process.exit(2);
});
