#!/usr/bin/env npx tsx

// Verification script to test Priority Forge setup
// Checks: server connectivity, MCP endpoint, database

import * as fs from 'fs';
import * as path from 'path';
import * as http from 'http';
import * as os from 'os';
import { execSync, spawnSync } from 'child_process';

const SERVER_URL = 'http://127.0.0.1:3456';
const DATA_FILE = path.join(__dirname, '..', 'data', 'progress.json');
const HOME = os.homedir();
// MCP is registered via .mcp.json files in each working directory.
// Trust is pre-accepted by writing enabledMcpjsonServers to ~/.claude.json.
const CLAUDE_JSON = path.join(HOME, '.claude.json');
const CLAUDE_MD = path.join(HOME, '.claude', 'CLAUDE.md');
const AGENTS_MD = path.join(HOME, '.claude', 'AGENTS.md'); // legacy - should have been migrated

// Platform detection (mirrors configure-mcp.ts). WSL is reported as 'linux'
// by os.platform() but we surface it separately so users know which env
// Claude Code is operating from.
export type ResolvedPlatform = 'darwin' | 'linux' | 'linux-wsl' | 'win32';

export function detectPlatform(): ResolvedPlatform {
  const p = os.platform();
  if (p === 'darwin') return 'darwin';
  if (p === 'win32') return 'win32';
  if (p === 'linux') {
    if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP) return 'linux-wsl';
    try {
      const procVersion = fs.readFileSync('/proc/version', 'utf-8');
      if (/microsoft|wsl/i.test(procVersion)) return 'linux-wsl';
    } catch { /* not WSL */ }
    return 'linux';
  }
  return 'linux';
}

export function isWindowsMountPath(p: string): boolean {
  return /^\/mnt\/[a-zA-Z]\//.test(p);
}

export function resolveProxyInstallPath(platform: ResolvedPlatform = detectPlatform()): string {
  switch (platform) {
    case 'darwin':
      return path.join(HOME, 'Library', 'Application Support', 'priority-forge', 'mcp-proxy.js');
    case 'win32':
      return path.join(
        process.env.LOCALAPPDATA || path.join(HOME, 'AppData', 'Local'),
        'priority-forge', 'mcp-proxy.js'
      );
    case 'linux':
    case 'linux-wsl':
    default:
      return path.join(HOME, '.local', 'share', 'priority-forge', 'mcp-proxy.js');
  }
}

const PLATFORM = detectPlatform();
const IS_LINUX = PLATFORM === 'linux' || PLATFORM === 'linux-wsl';
const PROXY_INSTALL_PATH = resolveProxyInstallPath(PLATFORM);

interface CheckResult {
  name: string;
  passed: boolean;
  message: string;
}

const results: CheckResult[] = [];

function check(name: string, passed: boolean, message: string) {
  results.push({ name, passed, message });
  const icon = passed ? '✓' : '✗';
  const color = passed ? '\x1b[32m' : '\x1b[31m';
  console.log(`  ${color}${icon}\x1b[0m ${name}: ${message}`);
}

async function httpGet(url: string): Promise<{ status: number; body: string }> {
  return new Promise((resolve, reject) => {
    const req = http.get(url, (res) => {
      let body = '';
      res.on('data', (chunk) => body += chunk);
      res.on('end', () => resolve({ status: res.statusCode || 0, body }));
    });
    req.on('error', reject);
    req.setTimeout(5000, () => {
      req.destroy();
      reject(new Error('Request timeout'));
    });
  });
}

async function httpPost(url: string, data: any): Promise<{ status: number; body: string }> {
  return new Promise((resolve, reject) => {
    const postData = JSON.stringify(data);
    const urlObj = new URL(url);
    
    const req = http.request({
      hostname: urlObj.hostname,
      port: urlObj.port,
      path: urlObj.pathname,
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json',
        'Content-Length': Buffer.byteLength(postData)
      }
    }, (res) => {
      let body = '';
      res.on('data', (chunk) => body += chunk);
      res.on('end', () => resolve({ status: res.statusCode || 0, body }));
    });
    
    req.on('error', reject);
    req.setTimeout(5000, () => {
      req.destroy();
      reject(new Error('Request timeout'));
    });
    
    req.write(postData);
    req.end();
  });
}

async function main() {
  console.log('\n┌─────────────────────────────────────────┐');
  console.log('│     Verifying Priority Forge Setup      │');
  console.log('└─────────────────────────────────────────┘\n');

  // Platform line — surfaces WSL so the user can sanity-check which env
  // Claude Code sees. Distinguishes plain Linux from WSL even though
  // os.platform() returns 'linux' for both.
  const platformLabel = PLATFORM === 'linux-wsl'
    ? `linux (WSL${process.env.WSL_DISTRO_NAME ? `: ${process.env.WSL_DISTRO_NAME}` : ''})`
    : PLATFORM;
  check('Platform', true, platformLabel);

  // Cross-mount guard. /mnt/<drive>/ inside WSL silently breaks SQLite +
  // file watching. Surface as a hard fail unless the user overrode.
  const cwd = process.cwd();
  if (PLATFORM === 'linux-wsl' && isWindowsMountPath(cwd)) {
    if (process.env.PRIORITY_FORGE_ALLOW_CROSS_MOUNT === '1') {
      check('Repo location', true, `${cwd} (cross-mount override enabled — expect flaky writes)`);
    } else {
      check('Repo location', false,
        `${cwd} is on a Windows-mounted path — move repo into WSL home (~/priority-forge)`);
    }
  }

  // Check 0: MCP client config via .mcp.json + enabledMcpjsonServers
  // Claude Code discovers .mcp.json files in the working directory.
  // Trust is pre-accepted via enabledMcpjsonServers in ~/.claude.json.
  // We check the Desktop directory since that's the most common session working directory.
  const desktopDir = path.join(HOME, 'Desktop');
  const desktopMcpJson = path.join(desktopDir, '.mcp.json');

  // Check .mcp.json presence and content
  if (fs.existsSync(desktopMcpJson)) {
    try {
      const mcpJson = JSON.parse(fs.readFileSync(desktopMcpJson, 'utf-8'));
      const entry = mcpJson?.['priority-forge'];
      if (entry?.command === 'node' && Array.isArray(entry?.args) && entry.args.includes(PROXY_INSTALL_PATH)) {
        check('MCP .mcp.json (Desktop)', true, `~/Desktop/.mcp.json → ${PROXY_INSTALL_PATH}`);
      } else {
        check('MCP .mcp.json (Desktop)', false,
          `unexpected format: ${JSON.stringify(entry)} - run: npm run setup:mcp`);
      }
    } catch {
      check('MCP .mcp.json (Desktop)', false, '~/Desktop/.mcp.json is malformed JSON');
    }
  } else {
    check('MCP .mcp.json (Desktop)', false,
      `~/Desktop/.mcp.json not found - run: npm run setup:mcp`);
  }

  // Check trust pre-acceptance in ~/.claude.json
  if (fs.existsSync(CLAUDE_JSON)) {
    try {
      const claudeCfg = JSON.parse(fs.readFileSync(CLAUDE_JSON, 'utf-8'));

      // Warn if stale user-scope entry still exists
      if (claudeCfg?.mcpServers?.['priority-forge']) {
        check('MCP user-scope (stale)', false,
          'priority-forge in top-level mcpServers — this scope is NOT loaded by Claude Code. Run: npm run setup:mcp');
      }

      // Check trust pre-acceptance for Desktop
      const desktopEnabled: string[] = claudeCfg?.projects?.[desktopDir]?.enabledMcpjsonServers || [];
      if (desktopEnabled.includes('priority-forge')) {
        check('MCP trust (Desktop)', true, 'priority-forge pre-accepted in enabledMcpjsonServers');
      } else {
        check('MCP trust (Desktop)', false,
          'priority-forge not in enabledMcpjsonServers for Desktop — run: npm run setup:mcp');
      }

      // Show coverage: dirs with both .mcp.json and trust pre-accepted
      const projects = claudeCfg?.projects || {};
      const trusted = Object.entries(projects).filter(([, proj]: [string, any]) => {
        const enabled: string[] = proj?.enabledMcpjsonServers || [];
        return enabled.includes('priority-forge');
      }).map(([dir]) => dir);
      if (trusted.length > 1) {
        check('MCP trust coverage', true, `pre-accepted in ${trusted.length} directories`);
      }
    } catch {
      check('MCP trust config', false, '~/.claude.json is malformed JSON');
    }
  } else {
    check('MCP trust config', false, '~/.claude.json not found - run: npm run setup:mcp');
  }

  // Check stable proxy install
  if (fs.existsSync(PROXY_INSTALL_PATH)) {
    const result = spawnSync('node', ['--check', PROXY_INSTALL_PATH], { encoding: 'utf-8' });
    check('MCP stdio proxy (installed)', result.status === 0, result.status === 0
      ? PROXY_INSTALL_PATH
      : `syntax error: ${result.stderr}`);
  } else {
    check('MCP stdio proxy (installed)', false,
      `${PROXY_INSTALL_PATH} not found — run: npm run setup:mcp`);
  }

  // Warn if legacy ~/.claude/mcp.json still exists (misleading — won't auto-load tools)
  const legacyMcpJson = path.join(HOME, '.claude', 'mcp.json');
  if (fs.existsSync(legacyMcpJson)) {
    try {
      const legacy = JSON.parse(fs.readFileSync(legacyMcpJson, 'utf-8'));
      if (legacy?.mcpServers?.['priority-forge']) {
        check('Legacy ~/.claude/mcp.json', false,
          'priority-forge still in ~/.claude/mcp.json (project-level, NOT auto-loaded) - run: npm run setup:mcp to migrate');
      }
    } catch { /* ignore malformed legacy file */ }
  }

  // Check 0b: Agent rules in CLAUDE.md (not AGENTS.md)
  const hasCLAUDEMd = fs.existsSync(CLAUDE_MD);
  const hasLegacyAgentsMd = fs.existsSync(AGENTS_MD);
  if (hasCLAUDEMd && fs.readFileSync(CLAUDE_MD, 'utf-8').includes('PRIORITY_FORGE_START')) {
    check('Agent rules (CLAUDE.md)', true, '~/.claude/CLAUDE.md contains Priority Forge rules');
    if (hasLegacyAgentsMd) {
      check('Agent rules (legacy)', true, 'AGENTS.md still exists but CLAUDE.md takes precedence - safe to delete AGENTS.md');
    }
  } else if (hasLegacyAgentsMd) {
    check('Agent rules (CLAUDE.md)', false,
      'Rules are in AGENTS.md (legacy) but Claude Code reads CLAUDE.md - run: npm run setup:mcp to migrate');
  } else {
    check('Agent rules (CLAUDE.md)', false, '~/.claude/CLAUDE.md not found - run: npm run setup:mcp');
  }

  // Check 0c: Systemd / launchd services
  if (IS_LINUX) {
    for (const svc of ['priority-forge-backend', 'priority-forge-frontend']) {
      try {
        const out = execSync(`systemctl --user is-active ${svc} 2>/dev/null`, { encoding: 'utf-8' }).trim();
        check(`systemd: ${svc}`, out === 'active', out === 'active' ? 'active (auto-starts on boot)' : `status: ${out}`);
      } catch (err: any) {
        // WSL without systemd enabled fails with "Failed to connect to bus".
        // Modern Ubuntu-on-WSL2 supports systemd via /etc/wsl.conf, but it's
        // off by default — surface the specific fix instead of a generic
        // "not installed" message that sends users down the wrong path.
        const stderr = (err?.stderr?.toString() || err?.message || '').toLowerCase();
        const busMissing = PLATFORM === 'linux-wsl' &&
          (stderr.includes('failed to connect to bus') || stderr.includes('no such file'));
        if (busMissing) {
          check(`systemd: ${svc}`, false,
            'WSL systemd not enabled — add `[boot]\\nsystemd=true` to /etc/wsl.conf, then run `wsl --shutdown` from PowerShell');
        } else {
          check(`systemd: ${svc}`, false, 'not installed - run: bash scripts/install-systemd.sh install');
        }
      }
    }
  } else {
    for (const svc of ['com.priority-forge.backend', 'com.priority-forge.frontend']) {
      try {
        const out = execSync(`launchctl list ${svc} 2>/dev/null`, { encoding: 'utf-8' });
        check(`launchd: ${svc}`, out.includes(svc), 'loaded');
      } catch {
        check(`launchd: ${svc}`, false, 'not loaded - run: ./setup.sh install-launchd');
      }
    }
  }

  // Cursor-specific checks. Runs when ~/.cursor/ exists or --cursor is passed.
  // Separate from the Claude/Droid/Codex block above so the two paths are
  // independent; the Cursor installer writes its own files in its own locations.
  const forceCursor = process.argv.includes('--cursor');
  const cursorDir = path.join(HOME, '.cursor');
  if (forceCursor || fs.existsSync(cursorDir)) {
    const cursorMcpJson = path.join(cursorDir, 'mcp.json');
    const cursorRuleMdc = path.join(cursorDir, 'rules', 'priority-forge.mdc');
    const cursorHooksJson = path.join(cursorDir, 'hooks', 'hooks.json');
    const cursorHookScript = path.join(cursorDir, 'hooks', 'priority-forge', 'sessionStart.sh');
    const cursorRulesLegacy = path.join(HOME, '.cursorrules');
    const COMPETING_PM = /linear|jira|asana|clickup|monday|trello|todoist/i;

    // 1. ~/.cursor/mcp.json has a priority-forge entry pointing at the HTTP endpoint.
    if (fs.existsSync(cursorMcpJson)) {
      try {
        const cfg = JSON.parse(fs.readFileSync(cursorMcpJson, 'utf-8'));
        const entry = cfg?.mcpServers?.['priority-forge'];
        if (entry?.url === 'http://127.0.0.1:3456/mcp') {
          check('Cursor MCP config', true, '~/.cursor/mcp.json has priority-forge');
        } else if (entry) {
          check('Cursor MCP config', false,
            `priority-forge URL is ${entry.url} (expected http://127.0.0.1:3456/mcp) - run: npm run setup:mcp`);
        } else {
          check('Cursor MCP config', false, 'priority-forge not in ~/.cursor/mcp.json - run: npm run setup:mcp');
        }

        // Warn about competing PM MCPs sharing the same config. Non-failing.
        const competing = Object.keys(cfg?.mcpServers || {}).filter(
          (k) => k !== 'priority-forge' && COMPETING_PM.test(k)
        );
        if (competing.length > 0) {
          check('Cursor competing PM MCPs', true,
            `warn: ${competing.join(', ')} may shadow priority-forge for task ops`);
        }
      } catch {
        check('Cursor MCP config', false, '~/.cursor/mcp.json is malformed JSON');
      }
    } else {
      check('Cursor MCP config', false, '~/.cursor/mcp.json not found - run: npm run setup:mcp');
    }

    // 2. ~/.cursor/rules/priority-forge.mdc exists with alwaysApply frontmatter and our markers.
    if (fs.existsSync(cursorRuleMdc)) {
      const content = fs.readFileSync(cursorRuleMdc, 'utf-8');
      const hasFrontmatter = /^---[\s\S]*?alwaysApply:\s*true[\s\S]*?---/m.test(content);
      const hasMarker = content.includes('PRIORITY_FORGE_START');
      if (hasFrontmatter && hasMarker) {
        check('Cursor rule', true, '~/.cursor/rules/priority-forge.mdc (alwaysApply: true)');
      } else {
        const missing = [
          !hasFrontmatter && 'alwaysApply: true frontmatter',
          !hasMarker && 'PRIORITY_FORGE marker block',
        ].filter(Boolean).join(', ');
        check('Cursor rule', false, `${cursorRuleMdc} missing ${missing} - run: npm run setup:mcp`);
      }
    } else {
      check('Cursor rule', false, '~/.cursor/rules/priority-forge.mdc not found - run: npm run setup:mcp');
    }

    // 3. Legacy ~/.cursorrules no longer contains our markers (should have been migrated).
    if (fs.existsSync(cursorRulesLegacy)) {
      const legacy = fs.readFileSync(cursorRulesLegacy, 'utf-8');
      if (legacy.includes('PRIORITY_FORGE_START')) {
        check('Cursor legacy .cursorrules', false,
          'priority-forge block still in ~/.cursorrules - run: npm run setup:mcp to migrate to ~/.cursor/rules/');
      }
    }

    // 4. sessionStart hook is installed, registered, and executable.
    if (fs.existsSync(cursorHooksJson)) {
      try {
        const hooks = JSON.parse(fs.readFileSync(cursorHooksJson, 'utf-8'));
        const entries: Array<{ id?: string; command?: string }> = hooks?.sessionStart || [];
        const ourEntry = entries.find((e) => e?.id === 'priority-forge');
        if (ourEntry?.command && fs.existsSync(ourEntry.command)) {
          try {
            fs.accessSync(ourEntry.command, fs.constants.X_OK);
            check('Cursor sessionStart hook', true, `${ourEntry.command} (executable)`);
          } catch {
            check('Cursor sessionStart hook', false,
              `${ourEntry.command} exists but is not executable - run: chmod +x ${ourEntry.command}`);
          }
        } else if (ourEntry?.command) {
          check('Cursor sessionStart hook', false,
            `registered command ${ourEntry.command} does not exist - run: npm run setup:mcp`);
        } else {
          check('Cursor sessionStart hook', false,
            'priority-forge entry missing from ~/.cursor/hooks/hooks.json - run: npm run setup:mcp');
        }
      } catch {
        check('Cursor sessionStart hook', false, '~/.cursor/hooks/hooks.json is malformed JSON');
      }
    } else if (fs.existsSync(cursorHookScript)) {
      check('Cursor sessionStart hook', false,
        'hook script present but ~/.cursor/hooks/hooks.json missing - run: npm run setup:mcp');
    } else {
      check('Cursor sessionStart hook', false,
        '~/.cursor/hooks/hooks.json not found - run: npm run setup:mcp');
    }
  }

  // Check 1: Database file exists
  const dbExists = fs.existsSync(DATA_FILE);
  check('Database', dbExists, dbExists ? 'progress.json exists' : 'progress.json not found - run seed script');
  
  if (dbExists) {
    try {
      const data = JSON.parse(fs.readFileSync(DATA_FILE, 'utf-8'));
      const projectCount = data.projects?.length || 0;
      const taskCount = data.tasks?.length || 0;
      check('Database content', true, `${projectCount} projects, ${taskCount} tasks`);
    } catch {
      check('Database content', false, 'Failed to parse progress.json');
    }
  }
  
  // Check 2: Server health endpoint
  let serverRunning = false;
  try {
    const healthRes = await httpGet(`${SERVER_URL}/health`);
    serverRunning = healthRes.status === 200;
    if (serverRunning) {
      const health = JSON.parse(healthRes.body);
      check('Server', true, `Running (v${health.version || 'unknown'})`);
    } else {
      check('Server', false, `Unexpected status: ${healthRes.status}`);
    }
  } catch (err: any) {
    if (err.code === 'ECONNREFUSED') {
      check('Server', false, 'Not running - start with: npm run dev');
    } else {
      check('Server', false, `Error: ${err.message}`);
    }
  }
  
  // Check 3: MCP endpoint (only if server is running)
  if (serverRunning) {
    try {
      const mcpRes = await httpPost(`${SERVER_URL}/mcp`, {
        jsonrpc: '2.0',
        id: 1,
        method: 'initialize',
        params: {
          protocolVersion: '2024-11-05',
          capabilities: {},
          clientInfo: { name: 'verify-script', version: '1.0.0' }
        }
      });
      
      const mcpOk = mcpRes.status === 200;
      if (mcpOk) {
        const response = JSON.parse(mcpRes.body);
        if (response.result?.serverInfo) {
          check('MCP endpoint', true, `Protocol working (${response.result.serverInfo.name})`);
        } else if (response.error) {
          check('MCP endpoint', false, `Error: ${response.error.message}`);
        } else {
          check('MCP endpoint', true, 'Protocol responding');
        }
      } else {
        check('MCP endpoint', false, `Status: ${mcpRes.status}`);
      }
    } catch (err: any) {
      check('MCP endpoint', false, `Error: ${err.message}`);
    }
    
    // Check 4: REST API
    try {
      const statusRes = await httpGet(`${SERVER_URL}/status`);
      if (statusRes.status === 200) {
        check('REST API', true, '/status endpoint working');
      } else {
        check('REST API', false, `Status: ${statusRes.status}`);
      }
    } catch (err: any) {
      check('REST API', false, `Error: ${err.message}`);
    }

    // Check 5: Memory hub planes (non-failing — hub is optional)
    //
    // Surfaces the three planes (pulse / memory / orchestration) and any
    // warnings the hub raised about its own config (e.g. memory on but
    // backend=noop, XMEM_FORCE_NOOP set, orchestration on with a backend
    // that can't do CAS). Always reports `passed: true` so an installer
    // who has no interest in the hub still gets a clean verify run.
    try {
      const hubRes = await httpGet(`${SERVER_URL}/xmem/status`);
      if (hubRes.status === 200) {
        const s = JSON.parse(hubRes.body) as {
          enabled?: boolean;
          memory?: { enabled?: boolean; backend?: string; teamId?: string };
          pulse?: { emit?: boolean };
          orchestration?: { enabled?: boolean };
          warnings?: string[];
          health?: { ok?: boolean; backend?: string };
        };
        const memOn = s.memory?.enabled ?? s.enabled ?? false;
        const pulseOn = s.pulse?.emit ?? false;
        const orchOn = s.orchestration?.enabled ?? false;
        if (memOn) {
          const backend = s.memory?.backend ?? s.health?.backend ?? 'unknown';
          const team = s.memory?.teamId ?? '?';
          const healthIcon = s.health?.ok ? 'ok' : 'unhealthy';
          check('Memory hub',
            true,
            `on (backend=${backend}, team=${team}, pulse=${pulseOn}, orchestration=${orchOn}, health=${healthIcon})`);
        } else {
          check('Memory hub', true,
            `disabled (pulse=${pulseOn}, memory=off, orchestration=${orchOn}) — optional; run hub_configure to enable`);
        }
        if (Array.isArray(s.warnings) && s.warnings.length > 0) {
          for (const w of s.warnings) {
            check('Memory hub warning', true, w);
          }
        }
      } else {
        check('Memory hub', true, `status endpoint returned ${hubRes.status} (hub may not be wired; skipping)`);
      }
    } catch (err: any) {
      check('Memory hub', true, `status endpoint unreachable: ${err.message} (non-fatal)`);
    }
  }
  
  // Summary
  const passed = results.filter(r => r.passed).length;
  const total = results.length;
  const allPassed = passed === total;
  
  console.log('\n─────────────────────────────────────────');
  if (allPassed) {
    console.log(`\x1b[32m  All checks passed (${passed}/${total})\x1b[0m`);
  } else {
    console.log(`\x1b[33m  ${passed}/${total} checks passed\x1b[0m`);
    if (!serverRunning) {
      console.log('\n  To complete verification, start the server:');
      console.log('    npm run dev');
      console.log('\n  Then run verification again:');
      console.log('    npm run verify');
    }
  }
  console.log('');
  
  process.exit(allPassed ? 0 : 1);
}

// Only execute when invoked as a script. Importing verify.ts from tests
// (to exercise the exported platform helpers) should not trigger a full
// verification run — that would hit the live server and exit the process.
// require.main === module is reliable under tsx; the fallback compares
// argv[1] basename for environments where require.main is undefined.
const isMain = (typeof require !== 'undefined' && require.main === module) ||
  /verify\.ts$/.test(process.argv[1] || '');

if (isMain) {
  main().catch((err) => {
    console.error('Verification failed:', err);
    process.exit(1);
  });
}
