// ---------------------------------------------------------------------------
// eval-server.ts -- HTTP server for the eval UI
//
// DO NOT REORDER the `./boot-preflight.js` import below — it MUST be the
// first import in this file so `mergeStoredKeysIntoEnv()` runs before any
// provider module (Anthropic / OpenAI / OpenRouter SDK) captures
// `process.env.*_API_KEY` at module-load time. Moving it lower (or guarding
// it) silently breaks stored-key-only boots. See 0702 T-020/T-021.
// ---------------------------------------------------------------------------
// Side-effect import: populates process.env.*_API_KEY from the file-backed
// settings store BEFORE any downstream module reads those env vars. Must
// remain the first import — do not reorder.
import "./boot-preflight.js";
import * as http from "node:http";
import * as fs from "node:fs";
import * as path from "node:path";
import { fileURLToPath } from "node:url";
import { Router } from "./router.js";
import { sendJson, getStudioToken } from "./router.js";
import { registerRoutes } from "./api-routes.js";
import { proxyToPlatform, shouldProxyToPlatform } from "./platform-proxy.js";
import { registerImproveRoutes } from "./improve-routes.js";
import { registerModelCompareRoutes } from "./model-compare-routes.js";
import { registerSkillCreateRoutes } from "./skill-create-routes.js";
import { registerSweepRoutes } from "./sweep-routes.js";
import { registerIntegrationRoutes } from "./integration-routes.js";
import { registerScopeTransferRoutes } from "../studio/routes/index.js";
import { registerWorkspaceRoutes } from "./workspace-routes.js";
import { loadWorkspace, addProject, setActiveProject, projectIdFromPath } from "./workspace-store.js";
import { registerAuthoringRoutes } from "./authoring-routes.js";
import { registerPluginCliRoutes } from "./plugin-cli-routes.js";
import { registerGitRoutes } from "./git-routes.js";
import { registerDesktopOpenRoutes } from "./desktop-open-routes.js";
import { registerOauthGithubRoutes } from "./oauth-github-routes.js";
import { handleActiveTenant } from "./active-tenant-routes.js";
import { checkDistFreshness } from "./check-dist-freshness.js";
import { homedir } from "node:os";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const MIME_TYPES = {
    ".html": "text/html",
    ".js": "application/javascript",
    ".css": "text/css",
    ".json": "application/json",
    ".png": "image/png",
    ".svg": "image/svg+xml",
    ".ico": "image/x-icon",
    ".woff": "font/woff",
    ".woff2": "font/woff2",
    ".ttf": "font/ttf",
    ".map": "application/json",
    ".webp": "image/webp",
};
export async function startEvalServer(opts) {
    const router = new Router();
    const { port } = opts;
    const workspaceDir = opts.workspaceDir ??
        process.env.VSKILL_WORKSPACE_DIR ??
        path.join(homedir(), ".vskill");
    // 0698 T-013: resolve root from (in order): explicit opts.root (CLI --root),
    // or the active project in workspace.json. Legacy code paths that require a
    // non-empty root still get one — when there's no workspace and no --root,
    // we fall back to cwd so the server can boot with a minimal empty state.
    const root = opts.root ?? resolveActiveRoot(workspaceDir) ?? process.cwd();
    // Register workspace endpoints FIRST so legacy routes can consult the store.
    registerWorkspaceRoutes(router, { workspaceDir });
    // If --root was passed but workspace is empty, seed it with that project
    // so CLI parity holds (user sees the same sidebar with multi-project UI).
    if (opts.root) {
        seedWorkspaceFromRoot(workspaceDir, opts.root);
    }
    // Register API routes
    registerRoutes(router, root, opts.projectName);
    registerImproveRoutes(router, root);
    registerModelCompareRoutes(router, root);
    registerSkillCreateRoutes(router, root);
    registerSweepRoutes(router, root);
    registerIntegrationRoutes(router, root);
    registerScopeTransferRoutes(router, root);
    registerAuthoringRoutes(router, root);
    registerPluginCliRoutes(router, root);
    registerGitRoutes(router, root);
    registerDesktopOpenRoutes(router);
    // 0843 followup (2026-05-11): GitHub OAuth Authorization Code + PKCE flow
    // (replaces the Tauri device-flow IPC path, which was double-broken by
    // (a) Tauri 2.x ACL rejecting all custom commands at runtime and
    // (b) the vskill OAuth App having Device Flow disabled in GitHub settings).
    registerOauthGithubRoutes(router);
    // Static asset directory
    const staticDir = path.resolve(__dirname, "../eval-ui");
    const server = http.createServer(async (req, res) => {
        // Handle CORS preflight. Router exposes an optional `options` handler
        // declared on the class — type-checked, no `as any` cast needed.
        if (req.method === "OPTIONS") {
            const apiRoutes = req.url?.startsWith("/api/");
            if (apiRoutes && router.options) {
                router.options(req, res);
                return;
            }
            res.writeHead(204);
            res.end();
            return;
        }
        // Try API routes first
        const handled = await router.handle(req, res);
        if (handled)
            return;
        // 0839 T-011 / US-004 — `/__internal/active-tenant` is a loopback-only
        // GET/POST helper used by the Studio TenantPicker. Lives outside the
        // `/api/*` namespace by design (the studio-token gate only covers
        // `/api/*`; this path predates that gate and is guarded by the
        // loopback bind + an explicit remoteAddress check inside the handler).
        if ((req.url ?? "").startsWith("/__internal/active-tenant")) {
            const taken = await handleActiveTenant(req, res);
            if (taken)
                return;
        }
        // 0712 US-003 T-016B (default re-targeted in 0725): forward platform-owned
        // API surface (`/api/v1/skills/*` — check-updates, stream, etc.) to the
        // vskill-platform process. The studio frontend issues these as relative
        // URLs; in production they resolve via same-origin to the platform, but in
        // dev the studio is served by this eval-server (port-hashed, e.g. 3162)
        // and would otherwise 404. Target is `VSKILL_PLATFORM_URL` (default
        // https://verified-skill.com — production worker; mirrors api/client.ts
        // DEFAULT_BASE_URL). Local-platform devs override via
        // `VSKILL_PLATFORM_URL=http://localhost:3017`.
        if (shouldProxyToPlatform(req.url)) {
            await proxyToPlatform(req, res);
            return;
        }
        // 404 for unknown API routes
        if (req.url?.startsWith("/api/")) {
            sendJson(res, { error: "Not found" }, 404, req);
            return;
        }
        // Serve static files
        await serveStatic(req, res, staticDir);
    });
    // POST /api/shutdown — graceful self-stop, used by `vskill studio --force`
    // to free the port before restarting in-place. Replies before closing so
    // the caller sees a clean 200 instead of a connection reset. Registered
    // here (after `server` is constructed) so the handler can close it.
    // Loopback-only: the same CSRF gate the install/state routes use, since
    // any same-origin tab on a developer box could otherwise terminate studio.
    router.post("/api/shutdown", async (req, res) => {
        const addr = req.socket.remoteAddress ?? "";
        if (addr !== "127.0.0.1" && addr !== "::1" && addr !== "::ffff:127.0.0.1") {
            sendJson(res, { ok: false, error: "loopback-only" }, 403, req);
            return;
        }
        sendJson(res, { ok: true, shuttingDown: true });
        setTimeout(() => {
            try {
                server.close(() => process.exit(0));
            }
            catch {
                process.exit(0);
            }
            // Hard fallback if close() never fires (active websocket, hung handler).
            setTimeout(() => process.exit(0), 1000).unref();
        }, 50);
    });
    return new Promise((resolve) => {
        // 0836 US-001: bind explicit loopback so the kernel rejects non-loopback
        // connects (LAN, dual-stack `::`). The default Node behaviour (no host
        // arg) binds dual-stack including LAN-reachable interfaces — anyone on
        // the same Wi-Fi could reach the GitHub-token-injecting proxy.
        server.listen(port, "127.0.0.1", () => {
            // 0728: Warn when running compiled bundle predates source edits.
            // pkgRoot is two levels up from __dirname (dist/eval-server/ or src/eval-server/).
            const pkgRoot = path.resolve(__dirname, "..", "..");
            const freshness = checkDistFreshness(pkgRoot);
            if (freshness.stale) {
                console.log(`\n  \x1b[33m⚠️  Stale dist detected (${freshness.details}) — run \`npm run build\` and restart\x1b[0m`);
            }
            // Read the bound port from server.address() in case `port: 0` was
            // passed (the OS assigns a free port). The banner string MUST stay
            // visually consistent with prior versions for the bridge port-detector.
            const bound = server.address();
            const boundPort = bound && typeof bound === "object" ? bound.port : port;
            console.log(`\n  Skill Studio: http://localhost:${boundPort}\n`);
            // 0836 US-002 AC-US2-01/07: emit the Studio token banner so:
            //   - CLI users (`npx vskill studio`) can copy it for curl
            //   - The Tauri sidecar (sidecar.rs stdout parser) can capture it
            //     and stash on SharedSidecar.studio_token for the WebView IPC.
            // Token is per-process and lives only in eval-server memory.
            const token = getStudioToken();
            console.log(`  Studio token: ${token}\n`);
            resolve(server);
        });
    });
}
// ---------------------------------------------------------------------------
// 0698 T-013: workspace-aware root resolution + CLI --root seed.
// ---------------------------------------------------------------------------
function resolveActiveRoot(workspaceDir) {
    try {
        const ws = loadWorkspace(workspaceDir);
        if (!ws.activeProjectId)
            return null;
        const active = ws.projects.find((p) => p.id === ws.activeProjectId);
        return active ? active.path : null;
    }
    catch {
        return null;
    }
}
function seedWorkspaceFromRoot(workspaceDir, root) {
    try {
        let ws = loadWorkspace(workspaceDir);
        // When `vskill studio` is invoked with an explicit `--root` (or the
        // CLI-default of cwd), that project should become the active one even if
        // the workspace already has other registered projects. Otherwise the user
        // sees a stale "fixtures" pill top-left just because an earlier session
        // pinned it — confusing and wrong.
        const existing = ws.projects.find((p) => p.path === root);
        if (existing) {
            if (ws.activeProjectId !== existing.id) {
                setActiveProject(workspaceDir, ws, existing.id);
            }
            return;
        }
        ws = addProject(workspaceDir, ws, { path: root });
        // addProject auto-activates the first add, but when there are already
        // other projects it leaves the existing active alone. Force-activate.
        const id = projectIdFromPath(root);
        if (ws.activeProjectId !== id) {
            setActiveProject(workspaceDir, ws, id);
        }
    }
    catch {
        /* non-fatal — workspace is optional */
    }
}
async function serveStatic(req, res, staticDir) {
    let urlPath = new URL(req.url || "/", "http://localhost").pathname;
    if (urlPath === "/")
        urlPath = "/index.html";
    // Security: prevent path traversal
    const safePath = path.normalize(urlPath).replace(/^(\.\.[/\\])+/, "");
    const filePath = path.join(staticDir, safePath);
    // Ensure we stay within staticDir
    if (!filePath.startsWith(staticDir)) {
        res.writeHead(403);
        res.end("Forbidden");
        return;
    }
    try {
        const stat = fs.statSync(filePath);
        if (stat.isFile()) {
            const ext = path.extname(filePath);
            const contentType = MIME_TYPES[ext] || "application/octet-stream";
            // 0836 US-002 follow-up: HTML files need the per-process X-Studio-Token
            // injected so the browser tab (npx vskill studio) can authenticate
            // /api/* calls. Streaming the raw file would skip this — read + inject.
            // Non-HTML assets (JS/CSS/images) stream as before.
            if (ext === ".html") {
                const raw = fs.readFileSync(filePath, "utf-8");
                res.writeHead(200, { "Content-Type": contentType });
                res.end(injectStudioToken(raw));
                return;
            }
            res.writeHead(200, { "Content-Type": contentType });
            fs.createReadStream(filePath).pipe(res);
            return;
        }
    }
    catch {
        // File not found - SPA fallback
    }
    // SPA fallback: serve index.html for all unmatched routes
    const indexPath = path.join(staticDir, "index.html");
    try {
        const content = fs.readFileSync(indexPath, "utf-8");
        res.writeHead(200, { "Content-Type": "text/html" });
        res.end(injectStudioToken(content));
    }
    catch {
        res.writeHead(404);
        res.end("Not found — run 'npm run build:eval-ui' first");
    }
}
/**
 * 0836 US-002 follow-up: inject the per-process X-Studio-Token into the
 * served index.html so browser-mode (`npx vskill studio` → user's default
 * browser) can authenticate API calls. Previously only Tauri's IPC path
 * delivered the token; the browser tab had no way to obtain it and every
 * /api/* call returned 401.
 *
 * Safe against DNS rebinding because the only consumer of this HTML is
 * the same-origin loopback page. An attacker.com tab cannot read this
 * HTML cross-origin (browser-enforced).
 *
 * Token resolution order on the client (StudioTokenBridge.ts):
 *   1. window.__VSKILL_STUDIO_TOKEN__  ← injected here (works in both modes)
 *   2. Tauri IPC get_studio_token       ← desktop-only fallback
 */
function injectStudioToken(html) {
    const token = getStudioToken();
    // Use the marker pattern Tauri's CSP allows. Inline JSON ensures the
    // value never appears in a script src attribute and isn't interpreted
    // as a string literal that could break on quotes.
    const marker = `<script id="__vskill_studio_token__" type="application/json">${JSON.stringify({ token })}</script>`;
    // Inject just before </head>; fall back to prepending <body> if head not found.
    if (html.includes("</head>")) {
        return html.replace("</head>", `${marker}\n</head>`);
    }
    if (html.includes("<body")) {
        return html.replace(/<body([^>]*)>/, `<body$1>\n${marker}`);
    }
    // No <head> or <body>: prepend (shouldn't happen for a real SPA shell).
    return `${marker}\n${html}`;
}
//# sourceMappingURL=eval-server.js.map