import { randomUUID } from "node:crypto";
import { DEFAULT_PATCH_SCOPE_LIMITS, EXPECTED_CHECKS, WORKFLOW_HANDOFF_SCHEMA_VERSION, WORKFLOW_KINDS, validateWorkflowHandoffRequest, } from "@oscharko-dev/keiko-contracts/workflow-handoff";
import { createAuditRedactor } from "@oscharko-dev/keiko-evidence";
import { WorkspaceError } from "@oscharko-dev/keiko-workspace";
import { currentRedactionSecrets } from "./deps.js";
import { approvalTokenInputFor, contextPackStableIdForPacks, createApprovalToken, evidenceAtomIdsForPacks, readOnlyPathsForPacks, } from "./governed-workflow.js";
import { lookupGroundedTurn } from "./grounded-turn-registry.js";
import { startRun } from "./run-engine.js";
import { parseRunRequest } from "./run-request.js";
import { ActiveRunLimitError } from "./runs.js";
import { errorBody } from "./routes.js";
const MAX_BODY_BYTES = 256 * 1024;
const VERIFY_NOOP_MODEL = {
    call: () => Promise.reject(new Error("verify runs must not call the model")),
};
class BodyTooLargeError extends Error {
    constructor() {
        super("request body too large");
        this.name = "BodyTooLargeError";
    }
}
function readBody(req) {
    return 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);
    });
}
function isRecord(value) {
    return typeof value === "object" && value !== null && !Array.isArray(value);
}
function isExpectedCheck(value) {
    return typeof value === "string" && EXPECTED_CHECKS.includes(value);
}
function isWorkflowKind(value) {
    return typeof value === "string" && WORKFLOW_KINDS.includes(value);
}
function parseJsonRecord(raw) {
    let parsed;
    try {
        parsed = JSON.parse(raw);
    }
    catch {
        return { status: 400, body: errorBody("BAD_REQUEST", "Request body is not valid JSON.") };
    }
    if (!isRecord(parsed)) {
        return { status: 400, body: errorBody("BAD_REQUEST", "Request body must be a JSON object.") };
    }
    return parsed;
}
function isRouteResult(value) {
    return isRecord(value) && typeof value.status === "number" && "body" in value;
}
function badField(field, message) {
    return {
        status: 400,
        body: errorBody("BAD_REQUEST", `Field "${field}" ${message}.`),
    };
}
function requireString(body, field) {
    const value = body[field];
    if (typeof value !== "string" || value.length === 0) {
        return badField(field, "is required");
    }
    return value;
}
function requireNumber(body, field) {
    const value = body[field];
    if (typeof value !== "number" || !Number.isFinite(value)) {
        return badField(field, "must be a finite number");
    }
    return value;
}
function requireRecord(body, field) {
    const value = body[field];
    if (!isRecord(value)) {
        return badField(field, "must be an object");
    }
    return value;
}
function stringArrayField(body, field) {
    const value = body[field];
    if (!Array.isArray(value)) {
        return badField(field, "must be an array");
    }
    const parsed = [];
    for (const entry of value) {
        if (typeof entry !== "string" || entry.trim().length === 0) {
            return badField(field, "must contain non-empty strings");
        }
        parsed.push(entry);
    }
    return parsed;
}
function defaultExpectedChecks(workflowKind) {
    if (workflowKind === "unit-test-generation") {
        return ["tests"];
    }
    return ["verify"];
}
function parseExpectedChecks(body, workflowKind) {
    if (body.expectedChecks === undefined) {
        return defaultExpectedChecks(workflowKind);
    }
    const parsed = stringArrayField(body, "expectedChecks");
    if (isRouteResult(parsed)) {
        return parsed;
    }
    if (!parsed.every(isExpectedCheck)) {
        return {
            status: 400,
            body: errorBody("BAD_REQUEST", 'Field "expectedChecks" contains an unknown check.'),
        };
    }
    return parsed;
}
function parseWorkflowKindField(body) {
    const workflowKind = requireString(body, "workflowKind");
    if (isRouteResult(workflowKind)) {
        return workflowKind;
    }
    return isWorkflowKind(workflowKind)
        ? workflowKind
        : { status: 400, body: errorBody("BAD_REQUEST", 'Field "workflowKind" is invalid.') };
}
function parseUnknowns(body) {
    return body.unknowns === undefined ? [] : stringArrayField(body, "unknowns");
}
function parseBody(raw) {
    const body = parseJsonRecord(raw);
    if (isRouteResult(body)) {
        return body;
    }
    const workflowKind = parseWorkflowKindField(body);
    if (isRouteResult(workflowKind)) {
        return workflowKind;
    }
    const assistantMessageId = requireString(body, "assistantMessageId");
    if (isRouteResult(assistantMessageId)) {
        return assistantMessageId;
    }
    const modelId = requireString(body, "modelId");
    if (isRouteResult(modelId)) {
        return modelId;
    }
    const input = requireRecord(body, "input");
    if (isRouteResult(input)) {
        return input;
    }
    const editablePaths = stringArrayField(body, "editablePaths");
    if (isRouteResult(editablePaths)) {
        return editablePaths;
    }
    const unknowns = parseUnknowns(body);
    if (isRouteResult(unknowns)) {
        return unknowns;
    }
    const requestedAtMs = requireNumber(body, "requestedAtMs");
    if (isRouteResult(requestedAtMs)) {
        return requestedAtMs;
    }
    const expectedChecks = parseExpectedChecks(body, workflowKind);
    if (isRouteResult(expectedChecks)) {
        return expectedChecks;
    }
    return {
        assistantMessageId,
        modelId,
        workflowKind,
        input,
        editablePaths,
        expectedChecks,
        unknowns,
        requestedAtMs,
    };
}
function workspaceRootForTurn(record) {
    const roots = new Set(record?.packs.map((pack) => pack.scope.workspaceRoot) ?? []);
    if (roots.size !== 1) {
        return {
            status: 409,
            body: errorBody("BAD_REQUEST", "Grounded handoff requires evidence from a single connected workspace root."),
        };
    }
    const root = [...roots][0];
    if (typeof root !== "string" || root.length === 0) {
        return {
            status: 404,
            body: errorBody("NOT_FOUND", "Grounded handoff context is no longer available."),
        };
    }
    return root;
}
function runRequestFor(workflowKind, modelId, workspaceRoot, input) {
    const parsed = parseRunRequest(JSON.stringify({
        ...(workflowKind === "verification"
            ? { taskType: "verify" }
            : { workflowId: workflowKind }),
        modelId,
        input: { workspaceRoot, ...input },
    }));
    if ("code" in parsed) {
        return { status: 400, body: errorBody(parsed.code, parsed.message) };
    }
    return parsed;
}
function summaryDiscriminator(workflowKind) {
    if (workflowKind === "verification") {
        return { workflowId: undefined, taskType: "verify" };
    }
    return { workflowId: workflowKind, taskType: undefined };
}
function workflowLabel(workflowKind) {
    if (workflowKind === "unit-test-generation") {
        return "grounded unit-test generation";
    }
    if (workflowKind === "bug-investigation") {
        return "grounded bug investigation";
    }
    return "grounded verification";
}
function persistGroundedHandoffMessages(deps, chatId, workflowKind, runId) {
    const now = Date.now();
    const discriminator = summaryDiscriminator(workflowKind);
    const label = workflowLabel(workflowKind);
    const [user, summary] = deps.store.createMessages([
        {
            chatId,
            role: "user",
            content: `Requested ${label}.`,
            timestamp: now,
            runId: undefined,
            workflowId: undefined,
            workflowStatus: undefined,
            shortResult: undefined,
            taskType: undefined,
        },
        {
            chatId,
            role: "system",
            content: `Launched ${label}.`,
            timestamp: now + 1,
            runId,
            workflowId: discriminator.workflowId,
            workflowStatus: "running",
            shortResult: undefined,
            taskType: discriminator.taskType,
        },
    ]);
    if (user === undefined || summary === undefined) {
        throw new Error("createMessages returned fewer rows than expected");
    }
    return [user, summary];
}
function resolveRunModel(request, deps) {
    return request.kind === "verify" ? VERIFY_NOOP_MODEL : deps.modelPortFactory(request.modelId);
}
function engineContextFor(deps, request, model) {
    return {
        request,
        model,
        registry: deps.registry,
        evidence: {
            store: deps.evidenceStore,
            env: deps.env,
            additionalSecrets: currentRedactionSecrets(deps),
        },
        memoryVault: deps.memoryVault,
        memoryAuditRedactString: createAuditRedactor({ additionalSecrets: currentRedactionSecrets(deps) }, deps.env),
    };
}
function markSummaryFailed(deps, message, shortResult) {
    try {
        deps.store.updateMessage(message.id, { workflowStatus: "failed", shortResult });
    }
    catch {
        // best-effort compensation only
    }
}
function mapRunStartError(error) {
    if (error instanceof ActiveRunLimitError) {
        return { status: 429, body: errorBody("TOO_MANY_RUNS", "The active run limit is reached.") };
    }
    if (error instanceof WorkspaceError) {
        return {
            status: 400,
            body: errorBody("WORKSPACE_UNAVAILABLE", "The selected workspace could not be prepared: no recognized project workspace marker was found, or the target file could not be read."),
        };
    }
    throw error;
}
function buildGovernedHandoffRequest(body, record) {
    const patchScope = {
        schemaVersion: WORKFLOW_HANDOFF_SCHEMA_VERSION,
        editablePaths: body.editablePaths,
        readOnlyPaths: readOnlyPathsForPacks(record.packs, body.editablePaths),
        evidenceAtomIds: evidenceAtomIdsForPacks(record.packs),
        limits: DEFAULT_PATCH_SCOPE_LIMITS,
        expectedChecks: body.expectedChecks,
        unknowns: body.unknowns,
    };
    const draft = {
        schemaVersion: WORKFLOW_HANDOFF_SCHEMA_VERSION,
        contextPackStableId: contextPackStableIdForPacks(record.packs),
        workflowKind: body.workflowKind,
        patchScope,
        requestedAtMs: body.requestedAtMs,
        userApprovalToken: "0".repeat(64),
    };
    const request = {
        ...draft,
        userApprovalToken: createApprovalToken(approvalTokenInputFor(draft)),
    };
    const validation = validateWorkflowHandoffRequest(request);
    if (!validation.ok) {
        return {
            status: 400,
            body: errorBody("BAD_REQUEST", validation.reasons.join("; ")),
        };
    }
    return request;
}
async function parseGroundedHandoffBody(req) {
    try {
        return parseBody(await readBody(req));
    }
    catch (error) {
        if (error instanceof BodyTooLargeError) {
            return {
                status: 413,
                body: errorBody("PAYLOAD_TOO_LARGE", "Request body exceeds the size limit."),
            };
        }
        throw error;
    }
}
function resolveGroundedHandoffLaunch(body, deps) {
    const record = lookupGroundedTurn(body.assistantMessageId);
    if (record === undefined) {
        return {
            status: 404,
            body: errorBody("NOT_FOUND", "Grounded handoff context is no longer available."),
        };
    }
    const workspaceRoot = workspaceRootForTurn(record);
    if (isRouteResult(workspaceRoot)) {
        return workspaceRoot;
    }
    const governedHandoff = buildGovernedHandoffRequest(body, record);
    if (isRouteResult(governedHandoff)) {
        return governedHandoff;
    }
    const baseRequest = runRequestFor(body.workflowKind, body.modelId, workspaceRoot, body.input);
    if (isRouteResult(baseRequest)) {
        return baseRequest;
    }
    const request = {
        ...baseRequest,
        governedHandoff,
        governedHandoffSourceGroundedRunId: record.evidenceRunId,
    };
    const model = resolveRunModel(request, deps);
    if (model === undefined) {
        return { status: 400, body: errorBody("NO_MODEL", "No model provider is configured.") };
    }
    return { chatId: record.chatId, workflowKind: body.workflowKind, request, model };
}
function startGroundedWorkflowRun(deps, launch) {
    const runId = randomUUID();
    const messages = persistGroundedHandoffMessages(deps, launch.chatId, launch.workflowKind, runId);
    try {
        const run = startRun(engineContextFor(deps, launch.request, launch.model), deps.redactor, {
            runId,
        });
        return {
            status: 202,
            body: { run, messages },
        };
    }
    catch (error) {
        markSummaryFailed(deps, messages[1], "Run could not be started.");
        return mapRunStartError(error);
    }
}
export async function handleGroundedWorkflowHandoff(ctx, deps) {
    const body = await parseGroundedHandoffBody(ctx.req);
    if (isRouteResult(body)) {
        return body;
    }
    const launch = resolveGroundedHandoffLaunch(body, deps);
    return isRouteResult(launch) ? launch : startGroundedWorkflowRun(deps, launch);
}
