#!/usr/bin/env npx tsx
/**
 * Layer 4: Save as CLI Testing — "Save as CLI" Pipeline
 *
 * Tests the full browser init → write adapter → browser verify flow.
 * Validates that browser exploration can be crystallized into reusable CLI adapters.
 *
 * Usage:
 *   npx tsx autoresearch/eval-save.ts              # Run all tasks
 *   npx tsx autoresearch/eval-save.ts --task hn-top # Run single task
 */

import { execSync } from 'node:child_process';
import { readFileSync, writeFileSync, mkdirSync, readdirSync, existsSync, rmSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { homedir } from 'node:os';

const __dirname = dirname(fileURLToPath(import.meta.url));
const TASKS_FILE = join(__dirname, 'save-tasks.json');
const RESULTS_DIR = join(__dirname, 'results');
const USER_CLIS_DIR = join(homedir(), '.opencli', 'clis');

interface SaveTask {
  name: string;
  site: string;
  command: string;
  /** Inline adapter code (simple tasks) */
  adapter?: string;
  /** Path to adapter file relative to autoresearch/ dir (complex tasks — avoids JSON escape issues) */
  adapterFile?: string;
  judge: JudgeCriteria;
  set?: 'test';
  note?: string;
}

type JudgeCriteria =
  | { type: 'contains'; value: string }
  | { type: 'arrayMinLength'; minLength: number }
  | { type: 'nonEmpty' }
  | { type: 'matchesPattern'; pattern: string };

interface TaskResult {
  name: string;
  phase: 'init' | 'write' | 'verify' | 'judge';
  passed: boolean;
  duration: number;
  error?: string;
  set: 'train' | 'test';
}

function judge(criteria: JudgeCriteria, output: string): boolean {
  try {
    switch (criteria.type) {
      case 'contains':
        return output.toLowerCase().includes(criteria.value.toLowerCase());
      case 'arrayMinLength': {
        // browser verify outputs table text; try JSON parse first, then count non-empty lines
        try {
          const arr = JSON.parse(output);
          if (Array.isArray(arr)) return arr.length >= criteria.minLength;
        } catch { /* not JSON — try line counting */ }
        // Table output: count data rows (skip header, separator, empty lines)
        const lines = output.split('\n').filter(l => l.trim() && !l.startsWith('─') && !l.startsWith('┌') && !l.startsWith('└') && !l.startsWith('├'));
        // Subtract header row
        const dataLines = lines.length > 1 ? lines.length - 1 : 0;
        return dataLines >= criteria.minLength;
      }
      case 'nonEmpty':
        return output.trim().length > 0 && output.trim() !== 'null' && output.trim() !== 'undefined';
      case 'matchesPattern':
        return new RegExp(criteria.pattern).test(output);
      default:
        return false;
    }
  } catch {
    return false;
  }
}

const PROJECT_ROOT = join(__dirname, '..');

/** Run a command, using the local built entrypoint instead of global opencli for consistency */
function runCommand(cmd: string, timeout = 30000): string {
  // Use local build so tests always run against the current source
  const localCmd = cmd.replace(/^opencli /, `node dist/src/main.js `);
  try {
    return execSync(localCmd, {
      cwd: PROJECT_ROOT,
      timeout,
      encoding: 'utf-8',
      env: process.env,
      stdio: ['pipe', 'pipe', 'pipe'],
    }).trim();
  } catch (err: any) {
    return err.stdout?.trim() || err.stderr?.trim() || '';
  }
}

function cleanupAdapter(site: string, command: string): void {
  const siteDir = join(USER_CLIS_DIR, site);
  const filePath = join(siteDir, `${command}.ts`);
  try {
    if (existsSync(filePath)) rmSync(filePath);
    // Remove site dir if empty
    if (existsSync(siteDir)) {
      const remaining = readdirSync(siteDir);
      if (remaining.length === 0) rmSync(siteDir, { recursive: true });
    }
  } catch { /* best effort */ }
}

function runTask(task: SaveTask): TaskResult {
  const start = Date.now();
  const { site, command } = task;
  const adapterDir = join(USER_CLIS_DIR, site);
  const adapterPath = join(adapterDir, `${command}.ts`);

  // Cleanup any leftover from previous runs
  cleanupAdapter(site, command);

  try {
    // Phase 1: init — create scaffold
    const initOutput = runCommand(`opencli browser init ${site}/${command}`);
    if (!existsSync(adapterPath)) {
      return {
        name: task.name, phase: 'init', passed: false,
        duration: Date.now() - start,
        error: `init failed: file not created. Output: ${initOutput.slice(0, 100)}`,
        set: task.set === 'test' ? 'test' : 'train',
      };
    }

    // Phase 2: write — overwrite scaffold with real adapter code
    if (task.adapterFile) {
      // Read from file (complex adapters — avoids JSON string escape issues)
      const srcPath = join(__dirname, task.adapterFile);
      const code = readFileSync(srcPath, 'utf-8');
      writeFileSync(adapterPath, code, 'utf-8');
    } else if (task.adapter) {
      writeFileSync(adapterPath, task.adapter, 'utf-8');
    }

    // Phase 3: verify — run the adapter via browser verify
    const verifyOutput = runCommand(
      `opencli browser verify ${site}/${command}`,
      45000, // longer timeout for network calls
    );

    if (verifyOutput.includes('✗ Adapter failed')) {
      return {
        name: task.name, phase: 'verify', passed: false,
        duration: Date.now() - start,
        error: `verify failed: ${verifyOutput.slice(0, 200)}`,
        set: task.set === 'test' ? 'test' : 'train',
      };
    }

    // Phase 4: judge — check output quality
    const passed = judge(task.judge, verifyOutput);

    return {
      name: task.name,
      phase: 'judge',
      passed,
      duration: Date.now() - start,
      error: passed ? undefined : `Judge failed on output: ${verifyOutput.slice(0, 150)}`,
      set: task.set === 'test' ? 'test' : 'train',
    };
  } catch (err: any) {
    return {
      name: task.name, phase: 'verify', passed: false,
      duration: Date.now() - start,
      error: err.message?.slice(0, 150),
      set: task.set === 'test' ? 'test' : 'train',
    };
  } finally {
    // Always cleanup test adapters
    cleanupAdapter(site, command);
  }
}

function main() {
  const args = process.argv.slice(2);
  const singleTask = args.includes('--task') ? args[args.indexOf('--task') + 1] : null;

  const allTasks: SaveTask[] = JSON.parse(readFileSync(TASKS_FILE, 'utf-8'));
  const tasks = singleTask ? allTasks.filter(t => t.name === singleTask) : allTasks;

  if (tasks.length === 0) {
    console.error(`Task "${singleTask}" not found.`);
    process.exit(1);
  }

  console.log(`\n🧪 Layer 4: Save as CLI — ${tasks.length} tasks\n`);

  const results: TaskResult[] = [];

  for (let i = 0; i < tasks.length; i++) {
    const task = tasks[i];
    process.stdout.write(`  [${i + 1}/${tasks.length}] ${task.name}...`);

    const result = runTask(task);
    results.push(result);

    const icon = result.passed ? '✓' : '✗';
    const phase = result.passed ? '' : ` (${result.phase})`;
    console.log(` ${icon}${phase} (${(result.duration / 1000).toFixed(1)}s)`);
  }

  // Summary
  const trainResults = results.filter(r => r.set === 'train');
  const testResults = results.filter(r => r.set === 'test');
  const totalPassed = results.filter(r => r.passed).length;
  const trainPassed = trainResults.filter(r => r.passed).length;
  const testPassed = testResults.filter(r => r.passed).length;
  const totalDuration = results.reduce((s, r) => s + r.duration, 0);

  console.log(`\n${'─'.repeat(50)}`);
  console.log(`  Score:  ${totalPassed}/${results.length} (train: ${trainPassed}/${trainResults.length}, test: ${testPassed}/${testResults.length})`);
  console.log(`  Time:   ${Math.round(totalDuration / 1000)}s`);

  const failures = results.filter(r => !r.passed);
  if (failures.length > 0) {
    console.log(`\n  Failures:`);
    for (const f of failures) {
      console.log(`    ✗ ${f.name} [${f.phase}]: ${f.error ?? 'unknown'}`);
    }
  }
  console.log('');

  // Save result
  mkdirSync(RESULTS_DIR, { recursive: true });
  const existing = readdirSync(RESULTS_DIR).filter(f => f.startsWith('save-')).length;
  const roundNum = String(existing + 1).padStart(3, '0');
  const resultPath = join(RESULTS_DIR, `save-${roundNum}.json`);
  writeFileSync(resultPath, JSON.stringify({
    timestamp: new Date().toISOString(),
    score: `${totalPassed}/${results.length}`,
    trainScore: `${trainPassed}/${trainResults.length}`,
    testScore: `${testPassed}/${testResults.length}`,
    duration: `${Math.round(totalDuration / 1000)}s`,
    tasks: results,
  }, null, 2), 'utf-8');
  console.log(`  Results saved to: ${resultPath}`);
  console.log(`\nSCORE=${totalPassed}/${results.length}`);
}

main();
