#!/usr/bin/env node

// watch-cron.mjs — Standalone cron wrapper for continuous security scanning
//
// Usage:  node watch-cron.mjs [--config <path>]
// Config: reports/watch/config.json (created on first run)
// Output: reports/watch/latest.json
// Exit:   0 = all ALLOW, 1 = any WARNING, 2 = any BLOCK

import { resolve, join, dirname, basename } from 'node:path';
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { spawnSync } from 'node:child_process';
import { tmpdir } from 'node:os';

const __dirname = dirname(fileURLToPath(import.meta.url));
const PLUGIN_ROOT = dirname(__dirname);
const ORCHESTRATOR = join(__dirname, 'scan-orchestrator.mjs');
const DEFAULT_CONFIG = join(PLUGIN_ROOT, 'reports', 'watch', 'config.json');
const WATCH_DIR = join(PLUGIN_ROOT, 'reports', 'watch');
const SCAN_TIMEOUT = 300_000; // 5 minutes per target

// --- Config ---

const CONFIG_TEMPLATE = {
  targets: [
    { path: '/absolute/path/to/project', label: 'my-project' }
  ],
  options: {
    baseline: true,
    saveBaseline: true
  }
};

function parseArgs(argv) {
  const args = { config: DEFAULT_CONFIG };
  for (let i = 2; i < argv.length; i++) {
    if (argv[i] === '--config' && argv[i + 1]) {
      args.config = resolve(argv[++i]);
    }
  }
  return args;
}

function loadConfig(configPath) {
  if (!existsSync(configPath)) {
    mkdirSync(dirname(configPath), { recursive: true });
    writeFileSync(configPath, JSON.stringify(CONFIG_TEMPLATE, null, 2) + '\n');
    console.log(`No watch config found. Created template at:\n  ${configPath}\n`);
    console.log('Edit it to add your watch targets (use absolute paths), then re-run.');
    return null;
  }

  try {
    const config = JSON.parse(readFileSync(configPath, 'utf8'));
    if (!Array.isArray(config.targets) || config.targets.length === 0) {
      console.log('Watch config has no targets. Add at least one target to config.targets[].');
      return null;
    }
    return config;
  } catch (err) {
    console.error(`Failed to parse config: ${err.message}`);
    return null;
  }
}

// --- Scan Execution ---

function runScan(target, options, pluginRoot) {
  const label = target.label || basename(target.path);
  const tmpFile = join(tmpdir(), `llm-security-watch-${Date.now()}-${label}.json`);

  const args = [ORCHESTRATOR, target.path, '--output-file', tmpFile];
  if (options.baseline !== false) args.push('--baseline');
  if (options.saveBaseline !== false) args.push('--save-baseline');

  const result = spawnSync(process.execPath, args, {
    encoding: 'utf8',
    timeout: SCAN_TIMEOUT,
    cwd: pluginRoot
  });

  const entry = {
    path: target.path,
    label,
    verdict: null,
    risk_score: null,
    counts: null,
    diff: null,
    error: null,
    exit_code: result.status
  };

  if (result.error) {
    entry.error = result.error.code === 'ETIMEDOUT' ? 'timeout' : result.error.message;
    return entry;
  }

  // Parse compact aggregate from stdout (when --output-file is used)
  try {
    const stdout = JSON.parse(result.stdout);
    if (stdout.aggregate) {
      entry.verdict = stdout.aggregate.verdict;
      entry.risk_score = stdout.aggregate.risk_score;
      entry.counts = stdout.aggregate.counts;
    }
  } catch {
    // stdout may be empty or non-JSON on error
  }

  // Read full output for diff data
  try {
    if (existsSync(tmpFile)) {
      const full = JSON.parse(readFileSync(tmpFile, 'utf8'));
      if (full.diff) {
        entry.diff = {
          new: full.diff.summary?.new ?? 0,
          resolved: full.diff.summary?.resolved ?? 0,
          unchanged: full.diff.summary?.unchanged ?? 0,
          moved: full.diff.summary?.moved ?? 0
        };
      }
    }
  } catch {
    // diff data is optional
  }

  // Cleanup temp file
  try {
    if (existsSync(tmpFile)) unlinkSync(tmpFile);
  } catch {
    // best effort
  }

  return entry;
}

// --- Summary ---

function buildSummary(results, startTime) {
  const verdictRank = { ALLOW: 0, WARNING: 1, BLOCK: 2 };
  let worst = 'ALLOW';

  for (const r of results) {
    if (r.verdict && verdictRank[r.verdict] > verdictRank[worst]) {
      worst = r.verdict;
    }
  }

  return {
    meta: {
      timestamp: new Date().toISOString(),
      duration_ms: Date.now() - startTime,
      targets_scanned: results.length,
      targets_ok: results.filter(r => !r.error).length,
      targets_failed: results.filter(r => r.error).length
    },
    worst_verdict: worst,
    targets: results
  };
}

function formatStdout(summary) {
  const lines = [`[llm-security watch] ${summary.meta.timestamp}`];

  for (const t of summary.targets) {
    if (t.error) {
      lines.push(`  ${t.label}: ERROR (${t.error})`);
    } else {
      const score = t.risk_score != null ? `score ${t.risk_score}` : 'no score';
      let delta = 'baseline scan';
      if (t.diff) {
        const parts = [];
        if (t.diff.new > 0) parts.push(`${t.diff.new} new`);
        if (t.diff.resolved > 0) parts.push(`${t.diff.resolved} resolved`);
        delta = parts.length > 0 ? parts.join(', ') : 'no changes';
      }
      lines.push(`  ${t.label}: ${t.verdict} (${score}) — ${delta}`);
    }
  }

  const outputPath = join(WATCH_DIR, 'latest.json');
  lines.push(`Worst: ${summary.worst_verdict} | Output: ${outputPath}`);
  return lines.join('\n');
}

// --- Main ---

function main() {
  const args = parseArgs(process.argv);
  const config = loadConfig(args.config);
  if (!config) process.exit(0);

  if (!existsSync(ORCHESTRATOR)) {
    console.error(`Scan orchestrator not found: ${ORCHESTRATOR}`);
    process.exit(1);
  }

  const options = config.options || {};
  const startTime = Date.now();
  const results = [];

  for (const target of config.targets) {
    if (!target.path) {
      console.error('  Skipping target with no path');
      continue;
    }
    results.push(runScan(target, options, PLUGIN_ROOT));
  }

  const summary = buildSummary(results, startTime);

  // Write output
  mkdirSync(WATCH_DIR, { recursive: true });
  writeFileSync(join(WATCH_DIR, 'latest.json'), JSON.stringify(summary, null, 2) + '\n');

  // Print to stdout
  console.log(formatStdout(summary));

  // Exit with worst verdict code
  const exitCodes = { ALLOW: 0, WARNING: 1, BLOCK: 2 };
  process.exit(exitCodes[summary.worst_verdict] || 0);
}

main();
