#!/usr/bin/env node

import path from "node:path";
import { createServer } from "node:http";
import { readFile, readdir, stat, writeFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { openInBrowser } from "../scripts/claude_cli.mjs";

const execFileAsync = promisify(execFile);
const METADATA_FILES = new Set(["transcript.md", "user_notes.md", "metrics.json"]);
const TEXT_EXTENSIONS = new Set([".txt", ".md", ".json", ".csv", ".py", ".js", ".ts", ".tsx", ".jsx", ".yaml", ".yml", ".xml", ".html", ".css", ".sh", ".rb", ".go", ".rs", ".java", ".c", ".cpp", ".h", ".hpp", ".sql", ".r", ".toml", ".mjs"]);
const IMAGE_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"]);
const MIME_OVERRIDES = {
  ".svg": "image/svg+xml",
  ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
  ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
  ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
};

function getMimeType(targetPath) {
  const extension = path.extname(targetPath).toLowerCase();
  return MIME_OVERRIDES[extension] ?? "application/octet-stream";
}

async function pathExists(targetPath) {
  try {
    await stat(targetPath);
    return true;
  } catch {
    return false;
  }
}

async function readJsonIfExists(targetPath) {
  if (!(await pathExists(targetPath))) return null;
  try {
    return JSON.parse(await readFile(targetPath, "utf8"));
  } catch {
    return null;
  }
}

export async function findRuns(workspace) {
  const runs = [];
  await findRunsRecursive(workspace, workspace, runs);
  runs.sort((a, b) => (a.eval_id ?? Number.MAX_SAFE_INTEGER) - (b.eval_id ?? Number.MAX_SAFE_INTEGER) || a.id.localeCompare(b.id));
  return runs;
}

async function findRunsRecursive(root, current, runs) {
  if (!(await pathExists(current))) return;
  const currentStat = await stat(current);
  if (!currentStat.isDirectory()) return;
  const outputsDir = path.join(current, "outputs");
  if (await pathExists(outputsDir)) {
    const builtRun = await buildRun(root, current);
    if (builtRun) {
      runs.push(builtRun);
    }
    return;
  }
  const entries = await readdir(current, { withFileTypes: true });
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
    if (!entry.isDirectory()) continue;
    if (new Set(["node_modules", ".git", "__pycache__", "skill", "inputs"]).has(entry.name)) continue;
    await findRunsRecursive(root, path.join(current, entry.name), runs);
  }
}

async function buildRun(root, runDir) {
  let prompt = "";
  let evalId = null;
  for (const candidate of [path.join(runDir, "eval_metadata.json"), path.join(path.dirname(runDir), "eval_metadata.json")]) {
    const metadata = await readJsonIfExists(candidate);
    if (metadata?.prompt) {
      prompt = metadata.prompt;
      evalId = metadata.eval_id ?? null;
      break;
    }
  }
  if (!prompt) {
    for (const candidate of [path.join(runDir, "transcript.md"), path.join(runDir, "outputs", "transcript.md")]) {
      if (!(await pathExists(candidate))) continue;
      const text = await readFile(candidate, "utf8");
      const match = text.match(/## Eval Prompt\n\n([\s\S]*?)(?=\n##|$)/);
      if (match?.[1]) {
        prompt = match[1].trim();
        break;
      }
    }
  }
  if (!prompt) {
    prompt = "(No prompt found)";
  }
  const outputFiles = [];
  const outputsDir = path.join(runDir, "outputs");
  if (await pathExists(outputsDir)) {
    const entries = await readdir(outputsDir, { withFileTypes: true });
    for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
      if (!entry.isFile() || METADATA_FILES.has(entry.name)) continue;
      outputFiles.push(await embedFile(path.join(outputsDir, entry.name)));
    }
  }
  const grading = await readJsonIfExists(path.join(runDir, "grading.json")) ?? await readJsonIfExists(path.join(path.dirname(runDir), "grading.json"));
  return {
    id: path.relative(root, runDir).replaceAll(path.sep, "-"),
    prompt,
    eval_id: evalId,
    outputs: outputFiles,
    grading,
  };
}

async function embedFile(targetPath) {
  const extension = path.extname(targetPath).toLowerCase();
  if (TEXT_EXTENSIONS.has(extension)) {
    return { name: path.basename(targetPath), type: "text", content: await readFile(targetPath, "utf8").catch(() => "(Error reading file)") };
  }
  const raw = await readFile(targetPath).catch(() => null);
  if (!raw) {
    return { name: path.basename(targetPath), type: "error", content: "(Error reading file)" };
  }
  const mime = getMimeType(targetPath);
  const data = raw.toString("base64");
  if (IMAGE_EXTENSIONS.has(extension)) {
    return { name: path.basename(targetPath), type: "image", mime, data_uri: `data:${mime};base64,${data}` };
  }
  if (extension === ".pdf") {
    return { name: path.basename(targetPath), type: "pdf", data_uri: `data:${mime};base64,${data}` };
  }
  if (extension === ".xlsx") {
    return { name: path.basename(targetPath), type: "xlsx", data_b64: data };
  }
  return { name: path.basename(targetPath), type: "binary", mime, data_uri: `data:${mime};base64,${data}` };
}

export async function loadPreviousIteration(workspace) {
  const result = {};
  const feedback = await readJsonIfExists(path.join(workspace, "feedback.json"));
  const feedbackMap = new Map();
  for (const review of feedback?.reviews ?? []) {
    if (review?.run_id && String(review.feedback ?? "").trim()) {
      feedbackMap.set(review.run_id, review.feedback);
    }
  }
  for (const run of await findRuns(workspace)) {
    result[run.id] = {
      feedback: feedbackMap.get(run.id) ?? "",
      outputs: run.outputs ?? [],
    };
  }
  for (const [runId, reviewFeedback] of feedbackMap.entries()) {
    if (!result[runId]) {
      result[runId] = { feedback: reviewFeedback, outputs: [] };
    }
  }
  return result;
}

export async function generateHtml({ workspace, skillName, previous = null, benchmark = null }) {
  const templatePath = path.join(path.dirname(fileURLToPath(import.meta.url)), "viewer.html");
  const template = await readFile(templatePath, "utf8");
  const runs = await findRuns(workspace);
  const previousFeedback = {};
  const previousOutputs = {};
  if (previous) {
    for (const [runId, data] of Object.entries(previous)) {
      if (data.feedback) previousFeedback[runId] = data.feedback;
      if (Array.isArray(data.outputs) && data.outputs.length > 0) previousOutputs[runId] = data.outputs;
    }
  }
  const embedded = { skill_name: skillName, runs, previous_feedback: previousFeedback, previous_outputs: previousOutputs };
  if (benchmark) {
    embedded.benchmark = benchmark;
  }
  return template.replace("/*__EMBEDDED_DATA__*/", `const EMBEDDED_DATA = ${JSON.stringify(embedded)};`);
}

async function killPort(port) {
  try {
    const { stdout } = await execFileAsync("lsof", ["-ti", `:${port}`], { timeout: 5000 });
    for (const pid of stdout.split("\n").map((value) => value.trim()).filter(Boolean)) {
      try {
        process.kill(Number(pid), "SIGTERM");
      } catch {}
    }
  } catch {}
}

function parseArgs(args) {
  const flags = { _: [] };
  for (let i = 0; i < args.length; i += 1) {
    const item = args[i];
    if (!item.startsWith("-")) {
      flags._.push(item);
      continue;
    }
    const key = item.startsWith("--") ? item.slice(2) : item.slice(1);
    const next = args[i + 1];
    if (!next || next.startsWith("-")) {
      flags[key] = true;
      continue;
    }
    flags[key] = next;
    i += 1;
  }
  return flags;
}

async function main(argv = process.argv) {
  const args = parseArgs(argv.slice(2));
  const workspace = path.resolve(args._[0] ?? "");
  if (!workspace) {
    throw new Error("Usage: node generate_review.mjs <workspace-path> [--port PORT] [--skill-name NAME] [--static output.html]");
  }
  const skillName = args["skill-name"] ?? path.basename(workspace).replace(/-workspace$/, "");
  const previous = args["previous-workspace"] ? await loadPreviousIteration(path.resolve(args["previous-workspace"])) : {};
  const benchmark = args.benchmark ? await readJsonIfExists(path.resolve(args.benchmark)) : null;
  if (args.static || args.s) {
    const outputPath = path.resolve(args.static ?? args.s);
    await writeFile(outputPath, await generateHtml({ workspace, skillName, previous, benchmark }));
    process.stdout.write(`Static viewer written to: ${outputPath}\n`);
    return;
  }
  let port = Number(args.port ?? args.p ?? 3117);
  await killPort(port);
  const feedbackPath = path.join(workspace, "feedback.json");
  const server = createServer(async (req, res) => {
    try {
      if (req.method === "GET" && (req.url === "/" || req.url === "/index.html")) {
        const html = await generateHtml({ workspace, skillName, previous, benchmark });
        res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
        res.end(html);
        return;
      }
      if (req.method === "GET" && req.url === "/api/feedback") {
        const feedback = await readFile(feedbackPath, "utf8").catch(() => "{}");
        res.writeHead(200, { "Content-Type": "application/json" });
        res.end(feedback);
        return;
      }
      if (req.method === "POST" && req.url === "/api/feedback") {
        let body = "";
        req.setEncoding("utf8");
        for await (const chunk of req) {
          body += chunk;
        }
        const parsed = JSON.parse(body);
        if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.reviews)) {
          throw new Error("Expected JSON object with 'reviews' key");
        }
        await writeFile(feedbackPath, `${JSON.stringify(parsed, null, 2)}\n`);
        res.writeHead(200, { "Content-Type": "application/json" });
        res.end("{\"ok\":true}");
        return;
      }
      res.writeHead(404);
      res.end("Not found");
    } catch (error) {
      res.writeHead(500, { "Content-Type": "application/json" });
      res.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
    }
  });
  await new Promise((resolve) => {
    server.once("error", () => {
      server.listen(0, "127.0.0.1", resolve);
    });
    server.listen(port, "127.0.0.1", resolve);
  });
  const address = server.address();
  port = typeof address === "object" && address ? address.port : port;
  const url = `http://localhost:${port}`;
  process.stdout.write(`Eval Viewer\nURL: ${url}\nWorkspace: ${workspace}\nFeedback: ${feedbackPath}\n`);
  await openInBrowser(url);
}

const entryPath = fileURLToPath(import.meta.url);
if (process.argv[1] && path.resolve(process.argv[1]) === entryPath) {
  main().catch((error) => {
    process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
    process.exitCode = 1;
  });
}
