// Quality Intelligence run start + cancel BFF routes (Epic #270, Issue #273/#280).
//
//   * POST /api/quality-intelligence/runs        — start a run; responds with an SSE progress stream
//   * POST /api/quality-intelligence/runs/:id/cancel — cancel an in-flight run
//
// The start route validates the body, then writes a `text/event-stream` of
// `QualityIntelligenceRunStreamMessage`s as the run progresses (accepted → events → done|error).
// All stream payloads carry only ids / counts / safe enums — never prompts, model output, source
// content, or credentials. A client disconnect aborts the run via the registry.
import { randomUUID } from "node:crypto";
import { isAbsolute } from "node:path";
import { SSE_HEADERS } from "../sse.js";
import { STREAMING, } from "../routes.js";
import { executeQiRun, QiGenerationError, QiIngestionError } from "./runExecution.js";
import { qiRunRegistry } from "./runRegistry.js";
const MAX_BODY_BYTES = 2 * 1024 * 1024;
class BodyTooLargeError extends Error {
}
const readBody = (req) => new Promise((resolve, reject) => {
    const chunks = [];
    let total = 0;
    let capped = false;
    req.on("data", (chunk) => {
        total += chunk.length;
        if (total > MAX_BODY_BYTES) {
            if (!capped) {
                capped = true;
                chunks.length = 0;
                reject(new BodyTooLargeError());
                req.resume();
            }
            return;
        }
        chunks.push(chunk);
    });
    req.on("end", () => {
        if (!capped)
            resolve(Buffer.concat(chunks).toString("utf8"));
    });
    req.on("error", reject);
});
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
const errorResult = (status, code, message) => ({
    status,
    body: { error: { code, message } },
});
function validateCapsuleSource(label, raw) {
    if (typeof raw.capsuleId !== "string" || raw.capsuleId.trim().length === 0) {
        return errorResult(400, "QI_BAD_REQUEST", "A capsule source requires a non-empty capsuleId.");
    }
    return { kind: "capsule", label, capsuleId: raw.capsuleId };
}
function validateCapsuleSetSource(label, raw) {
    if (typeof raw.capsuleSetId !== "string" || raw.capsuleSetId.trim().length === 0) {
        return errorResult(400, "QI_BAD_REQUEST", "A capsule-set source requires a non-empty capsuleSetId.");
    }
    return { kind: "capsule-set", label, capsuleSetId: raw.capsuleSetId };
}
function validateFigmaSnapshotSource(label, raw) {
    if (typeof raw.snapshotRunId !== "string" || raw.snapshotRunId.trim().length === 0) {
        return errorResult(400, "QI_BAD_REQUEST", "A figma-snapshot source requires a non-empty snapshotRunId.");
    }
    return { kind: "figma-snapshot", label, snapshotRunId: raw.snapshotRunId };
}
// Connector sources (Local Knowledge capsule / capsule-set, Figma snapshot). Split out so
// validateSource stays under the complexity budget as the source-kind union grows (Epic #710/#750).
function validateConnectorSource(label, raw) {
    if (raw.kind === "capsule") {
        return validateCapsuleSource(label, raw);
    }
    if (raw.kind === "capsule-set") {
        return validateCapsuleSetSource(label, raw);
    }
    if (raw.kind === "figma-snapshot") {
        return validateFigmaSnapshotSource(label, raw);
    }
    return undefined;
}
function validateSource(raw) {
    if (!isObject(raw) || typeof raw.label !== "string")
        return undefined;
    const label = raw.label;
    if (raw.kind === "requirements" && typeof raw.text === "string") {
        return { kind: "requirements", label, text: raw.text };
    }
    if (raw.kind === "workspace" && typeof raw.path === "string") {
        return { kind: "workspace", label, path: raw.path };
    }
    if (raw.kind === "file" && typeof raw.path === "string") {
        return { kind: "file", label, path: raw.path };
    }
    return validateConnectorSource(label, raw);
}
function isRouteResult(v) {
    return isObject(v) && typeof v.status === "number";
}
function validateSourceEntry(raw) {
    const source = validateSource(raw);
    if (source === undefined) {
        return errorResult(400, "QI_BAD_SOURCE", "A source entry is malformed.");
    }
    // A capsule (or other) field-level validation failure surfaces as a RouteResult — propagate it.
    if (isRouteResult(source)) {
        return source;
    }
    if (source.kind === "file" && !isAbsolute(source.path)) {
        return errorResult(400, "QI_BAD_SOURCE", "File source paths must be absolute local paths.");
    }
    return source;
}
function collectSources(rawSources) {
    const sources = [];
    for (const raw of rawSources) {
        // validateSourceEntry adds the absolute-path guard for file sources (#791) on top of the
        // shape + capsule validation, surfacing any failure as a RouteResult.
        const source = validateSourceEntry(raw);
        if (isRouteResult(source)) {
            return { ok: false, result: source };
        }
        sources.push(source);
    }
    return { ok: true, sources };
}
function parseOptionalSeed(value) {
    if (value === undefined)
        return undefined;
    return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
}
function buildStartRequest(sources, profileId, modelId, seed) {
    return {
        sources,
        ...(profileId ? { profileId } : {}),
        ...(modelId ? { modelId } : {}),
        ...(seed !== undefined ? { seed } : {}),
    };
}
function validateRequest(parsed) {
    if (!isObject(parsed) || !Array.isArray(parsed.sources) || parsed.sources.length === 0) {
        return {
            ok: false,
            result: errorResult(400, "QI_BAD_REQUEST", "At least one source is required."),
        };
    }
    const collected = collectSources(parsed.sources);
    if (!collected.ok)
        return collected;
    const profileId = typeof parsed.profileId === "string" ? parsed.profileId : undefined;
    const modelId = typeof parsed.modelId === "string" ? parsed.modelId : undefined;
    const seed = parseOptionalSeed(parsed.seed);
    if (seed === null) {
        return {
            ok: false,
            result: errorResult(400, "QI_BAD_REQUEST", "Seed must be a non-negative safe integer when provided."),
        };
    }
    return {
        ok: true,
        request: buildStartRequest(collected.sources, profileId, modelId, seed),
    };
}
async function parseStartBody(req) {
    let raw;
    try {
        raw = await readBody(req);
    }
    catch (error) {
        return error instanceof BodyTooLargeError
            ? { ok: false, result: errorResult(413, "QI_BODY_TOO_LARGE", "Request body is too large.") }
            : { ok: false, result: errorResult(400, "QI_BAD_REQUEST", "Could not read request body.") };
    }
    let parsed;
    try {
        parsed = JSON.parse(raw);
    }
    catch {
        return {
            ok: false,
            result: errorResult(400, "QI_BAD_REQUEST", "Request body is not valid JSON."),
        };
    }
    return validateRequest(parsed);
}
function toStreamEvent(event) {
    const p = event.payload;
    return {
        type: "event",
        kind: p.kind,
        sequence: event.sequence,
        ...("stageName" in p ? { stageName: p.stageName } : {}),
        ...("candidateId" in p ? { candidateId: String(p.candidateId) } : {}),
        ...("findingId" in p ? { findingId: String(p.findingId) } : {}),
        ...("reasonSummary" in p ? { reasonSummary: p.reasonSummary } : {}),
    };
}
function classifyStartError(error) {
    if (error instanceof QiIngestionError || error instanceof QiGenerationError) {
        return { code: error.code, message: error.message };
    }
    return { code: "QI_RUN_FAILED", message: "The Quality Intelligence run failed to complete." };
}
async function streamRunExecution(deps, request, runId, registeredAt, signal, write) {
    const totals = { candidates: 0, findings: 0, exports: 0 };
    let terminal = "failed";
    try {
        const summary = await executeQiRun({
            request,
            runId,
            deps,
            registeredAt,
            signal,
            onAccepted: (accepted) => {
                write({
                    type: "accepted",
                    runId: accepted.runId,
                    requestedAt: accepted.requestedAt,
                    sourceCount: accepted.sourceCount,
                    atomCount: accepted.atomCount,
                    ...(accepted.droppedSourceCount > 0
                        ? { droppedSourceCount: accepted.droppedSourceCount }
                        : {}),
                    ...(accepted.skippedSources.length > 0
                        ? { skippedSources: accepted.skippedSources }
                        : {}),
                });
            },
            onEvent: (event) => {
                if (event.payload.kind === "candidate:proposed")
                    totals.candidates += 1;
                if (event.payload.kind === "finding:recorded")
                    totals.findings += 1;
                qiRunRegistry.updateTotals(runId, totals);
                write(toStreamEvent(event));
            },
        });
        terminal = summary.status;
        write({ type: "done", runId, status: summary.status, totals });
    }
    catch (error) {
        const { code, message } = classifyStartError(error);
        write({ type: "error", code, message });
    }
    finally {
        qiRunRegistry.complete(runId, terminal);
    }
}
export async function handleStartQiRun(ctx, deps) {
    const parsed = await parseStartBody(ctx.req);
    if (!parsed.ok)
        return parsed.result;
    const runId = `qi-run-${randomUUID()}`;
    const registeredAt = new Date().toISOString();
    ctx.res.writeHead(200, { ...SSE_HEADERS, "X-Accel-Buffering": "no" });
    const write = (message) => {
        ctx.res.write(`data: ${JSON.stringify(message)}\n\n`);
    };
    const controller = qiRunRegistry.register(runId, registeredAt);
    ctx.res.on("close", () => {
        controller.abort();
    });
    await streamRunExecution(deps, parsed.request, runId, registeredAt, controller.signal, write);
    ctx.res.end();
    return STREAMING;
}
export function handleCancelQiRun(ctx, _deps) {
    const { id } = ctx.params;
    if (id === undefined || id.trim().length === 0) {
        return errorResult(400, "QI_BAD_REQUEST", "Run id is required.");
    }
    const cancelled = qiRunRegistry.cancel(id);
    return cancelled
        ? { status: 200, body: { cancelled: true, runId: id } }
        : errorResult(404, "QI_NOT_ACTIVE", "No in-flight run with that id.");
}
export const QI_RUN_EXECUTION_ROUTE_GROUP = [
    { method: "POST", pattern: "/api/quality-intelligence/runs", handler: handleStartQiRun },
    {
        method: "POST",
        pattern: "/api/quality-intelligence/runs/:id/cancel",
        handler: handleCancelQiRun,
    },
];
