var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __asyncValues = (this && this.__asyncValues) || function (o) {
    if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
    var m = o[Symbol.asyncIterator], i;
    return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
    function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
    function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
};
import { logger, Filters } from "adminforth";
import { randomUUID } from "crypto";
import { VegaLiteStreamBuffer } from "../domain/vegaLiteStreamBuffer.js";
import { buildAgentTurnSystemPrompt } from "../domain/systemPrompt.js";
import { getErrorMessage, isAbortError } from "../shared/errors.js";
function getApprovalDecision(input) {
    return "approvalDecision" in input
        && (input.approvalDecision === "approve" || input.approvalDecision === "reject")
        ? input.approvalDecision
        : undefined;
}
function buildHitlDecision(decision, prompt) {
    if (decision === "approve") {
        return { type: "approve" };
    }
    return {
        type: "reject",
        message: prompt
            ? `User rejected the pending tool execution and sent a new instruction instead: ${prompt}`
            : "User rejected executing this tool",
    };
}
function buildHitlResumeValue(input) {
    return {
        decisions: Array.from({ length: input.count }, () => (buildHitlDecision(input.decision, input.prompt))),
    };
}
/**
 * Build the provider-agnostic resume payload for a human-in-the-loop turn.
 * Exported for unit testing; the LLM port wraps it into a LangGraph Command.
 */
export function buildResumeValue(input) {
    var _a;
    const interrupts = (_a = input.interrupts) !== null && _a !== void 0 ? _a : [];
    if (interrupts.length === 0) {
        throw new Error("No pending approval interrupt found for resume.");
    }
    if (interrupts.length === 1) {
        return buildHitlResumeValue({
            decision: input.decision,
            count: interrupts[0].count,
            prompt: input.prompt,
        });
    }
    return Object.fromEntries(interrupts.map((interrupt) => [
        interrupt.id,
        buildHitlResumeValue({
            decision: input.decision,
            count: interrupt.count,
            prompt: input.prompt,
        }),
    ]));
}
/**
 * Single entry point for running an agent turn: prepare (start/resume) → build
 * the prompt → stream from the LLM port → consume the typed stream into SSE
 * events → persist the result. Replaces the former AgentTurnService plus the
 * TurnLifecycle/Context/Prompt/Persistence/StreamConsumer + ModeResolver stack.
 */
export class RunTurnUseCase {
    constructor(deps) {
        this.deps = deps;
        this.pendingInterrupts = new Map();
    }
    resolveMode(modeName) {
        var _a;
        return (_a = this.deps.modes.find((mode) => mode.name === modeName)) !== null && _a !== void 0 ? _a : this.deps.modes[0];
    }
    buildContext(input, turnId) {
        var _a, _b;
        return {
            adminUser: input.adminUser,
            sessionId: input.sessionId,
            turnId,
            abortSignal: input.abortSignal,
            currentPage: input.currentPage,
            chatSurface: input.chatSurface,
            userTimeZone: (_a = input.userTimeZone) !== null && _a !== void 0 ? _a : "UTC",
            adminPublicOrigin: (_b = input.adminPublicOrigin) !== null && _b !== void 0 ? _b : this.deps.getAdminforth().config.baseUrlSlashed,
        };
    }
    /**
     * Resolve the pending HITL interrupts for a resume. With a persistent checkpointer the
     * checkpoint is authoritative (correct across instances and restarts); with the in-memory
     * MemorySaver the in-process cache is authoritative (single process, cannot be stale).
     */
    resolvePendingInterrupts(sessionId, mode) {
        return __awaiter(this, void 0, void 0, function* () {
            var _a;
            if (this.deps.hasPersistentCheckpointer) {
                // The in-process cache can be stale after a cross-instance resume, so it is intentionally
                // NOT consulted here. A failure to read the checkpoint is surfaced as a real error — never
                // masked as "no pending approval".
                try {
                    return yield this.deps.llm.getPendingInterrupts({
                        completionAdapter: mode.completionAdapter,
                        sessionId,
                    });
                }
                catch (error) {
                    logger.error(`Failed to read pending approval state from checkpoint for session "${sessionId}": ${getErrorMessage(error)}`);
                    throw error;
                }
            }
            return (_a = this.pendingInterrupts.get(sessionId)) !== null && _a !== void 0 ? _a : [];
        });
    }
    /** Verify the session exists and belongs to the requesting admin user. */
    assertSessionOwnership(sessionId, adminUser) {
        return __awaiter(this, void 0, void 0, function* () {
            const s = this.deps.sessionResource;
            const session = yield this.deps.getAdminforth().resource(s.resourceId).get([Filters.EQ(s.idField, sessionId)]);
            if (!session) {
                throw new Error(`Session "${sessionId}" not found.`);
            }
            if (session[s.askerIdField] !== adminUser.pk) {
                throw new Error(`Session "${sessionId}" does not belong to the current user.`);
            }
        });
    }
    /**
     * Prepare an edit/branch turn: validate ownership, resolve the fork checkpoint from
     * the previous turn, truncate the conversation after the edited turn, and reuse the
     * edited turn's id for the regenerated response.
     */
    prepareEditTurn(input) {
        return __awaiter(this, void 0, void 0, function* () {
            const sequenceDebugSink = this.deps.createDebugSink();
            const mode = this.resolveMode(input.modeName);
            const editTurnId = input.editTurnId;
            if (!this.deps.turnCheckpointsEnabled) {
                throw new Error("Edit/fork requires checkpointIdField to be configured on turnResource.");
            }
            yield this.assertSessionOwnership(input.sessionId, input.adminUser);
            const agentTurns = yield this.deps.sessions.getAgentTurns(input.sessionId);
            const targetIndex = agentTurns.findIndex((turn) => turn.id === editTurnId);
            if (targetIndex === -1) {
                throw new Error(`Turn "${editTurnId}" not found in session "${input.sessionId}".`);
            }
            const previous = agentTurns[targetIndex - 1];
            let branchFromCheckpointId;
            if (previous) {
                if (!previous.checkpointId) {
                    throw new Error(`Cannot edit this message: the previous turn has no stored checkpoint to branch from.`);
                }
                branchFromCheckpointId = previous.checkpointId;
            }
            else {
                // No earlier turn to fork from — drop the whole thread and start fresh.
                yield this.deps.llm.resetThreadCheckpoints({
                    completionAdapter: mode.completionAdapter,
                    sessionId: input.sessionId,
                });
            }
            // Language-detection context: the kept user messages just before the edited turn.
            const previousUserMessages = agentTurns
                .slice(Math.max(0, targetIndex - 2), targetIndex)
                .map((turn) => ({ text: turn.prompt }));
            // Editing supersedes any in-flight turn state for this session.
            this.deps.steerBuffer.clear(input.sessionId);
            this.pendingInterrupts.delete(input.sessionId);
            // Apply the edit + truncate (deletes later turns first, then rewrites the edited turn).
            yield this.deps.sessions.editTurnAndTruncateAfter({
                sessionId: input.sessionId,
                turnId: editTurnId,
                newPrompt: input.prompt,
            });
            yield this.deps.sessions.touchSession(input.sessionId);
            return {
                prompt: input.prompt,
                sessionId: input.sessionId,
                turnId: editTurnId,
                previousUserMessages,
                mode,
                context: this.buildContext(input, editTurnId),
                observability: { emit: input.emit, sequenceDebugSink },
                branchFromCheckpointId,
            };
        });
    }
    prepareTurn(input) {
        return __awaiter(this, void 0, void 0, function* () {
            const sequenceDebugSink = this.deps.createDebugSink();
            const mode = this.resolveMode(input.modeName);
            const approvalDecision = getApprovalDecision(input);
            const shouldResume = Boolean(approvalDecision);
            if (input.editTurnId && !shouldResume) {
                return this.prepareEditTurn(input);
            }
            let turnId;
            let previousUserMessages = [];
            let initialResponse;
            let resumeInterrupts;
            if (shouldResume) {
                resumeInterrupts = yield this.resolvePendingInterrupts(input.sessionId, mode);
                if (resumeInterrupts.length === 0) {
                    throw new Error(`No pending approval interrupt found for session "${input.sessionId}".`);
                }
                const resumeState = yield this.deps.sessions.getResumeState(input.sessionId);
                turnId = resumeState.turnId;
                initialResponse = resumeState.initialResponse;
            }
            else {
                previousUserMessages = yield this.deps.sessions.getPreviousUserMessages(input.sessionId);
                turnId = yield this.deps.sessions.createNewTurn(input.sessionId, input.prompt);
                yield this.deps.sessions.touchSession(input.sessionId);
            }
            return {
                prompt: input.prompt,
                sessionId: input.sessionId,
                turnId,
                previousUserMessages,
                mode,
                context: this.buildContext(input, turnId),
                observability: {
                    emit: input.emit,
                    sequenceDebugSink,
                },
                resume: shouldResume
                    ? { decision: approvalDecision, interrupts: resumeInterrupts }
                    : undefined,
                initialResponse,
            };
        });
    }
    buildMessages(prepared) {
        return __awaiter(this, void 0, void 0, function* () {
            const userLanguage = yield this.deps.llm
                .detectLanguage({
                completionAdapter: prepared.mode.completionAdapter,
                prompt: prepared.prompt,
                previousUserMessages: prepared.previousUserMessages,
            })
                .catch((error) => {
                var _a;
                if (((_a = prepared.context.abortSignal) === null || _a === void 0 ? void 0 : _a.aborted) || isAbortError(error)) {
                    throw error;
                }
                logger.warn(`Failed to detect user language: ${getErrorMessage(error)}`);
                return null;
            });
            const adminforth = this.deps.getAdminforth();
            const systemPrompt = buildAgentTurnSystemPrompt({
                agentSystemPrompt: yield this.deps.getAgentSystemPrompt(),
                adminUser: prepared.context.adminUser,
                usernameField: adminforth.config.auth.usernameField,
                userLanguage,
                chatSurface: prepared.context.chatSurface,
            });
            return [
                { role: "system", content: systemPrompt },
                { role: "user", content: prepared.prompt },
            ];
        });
    }
    handleInterrupt(prepared, interrupt, descriptors) {
        return __awaiter(this, void 0, void 0, function* () {
            var _a, _b, _c;
            if (!this.deps.hasPersistentCheckpointer) {
                const existing = (_a = this.pendingInterrupts.get(prepared.sessionId)) !== null && _a !== void 0 ? _a : [];
                const merged = new Map(existing.map((item) => [item.id, item.count]));
                for (const item of descriptors) {
                    merged.set(item.id, item.count);
                }
                this.pendingInterrupts.set(prepared.sessionId, [...merged.entries()].map(([id, count]) => ({ id, count })));
            }
            yield ((_c = (_b = prepared.observability).emit) === null || _c === void 0 ? void 0 : _c.call(_b, {
                type: "interrupt",
                sessionId: prepared.sessionId,
                interrupt,
            }));
        });
    }
    runAgentTurn(prepared) {
        return __awaiter(this, void 0, void 0, function* () {
            var _a, e_1, _b, _c;
            const streamInput = prepared.resume
                ? {
                    resume: buildResumeValue({
                        decision: prepared.resume.decision,
                        interrupts: prepared.resume.interrupts,
                        prompt: prepared.prompt,
                    }),
                }
                : { messages: yield this.buildMessages(prepared) };
            const stream = yield this.deps.llm.streamTurn({
                completionAdapter: prepared.mode.completionAdapter,
                input: streamInput,
                context: prepared.context,
                observability: prepared.observability,
                branchFromCheckpointId: prepared.branchFromCheckpointId,
            });
            const { emit } = prepared.observability;
            const abortSignal = prepared.context.abortSignal;
            const textBuffer = new VegaLiteStreamBuffer();
            let fullResponse = "";
            let interrupted = false;
            try {
                try {
                    for (var _d = true, stream_1 = __asyncValues(stream), stream_1_1; stream_1_1 = yield stream_1.next(), _a = stream_1_1.done, !_a; _d = true) {
                        _c = stream_1_1.value;
                        _d = false;
                        const chunk = _c;
                        if (abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.aborted) {
                            throw new DOMException("This operation was aborted", "AbortError");
                        }
                        if (chunk.kind === "interrupt") {
                            interrupted = true;
                            yield this.handleInterrupt(prepared, chunk.interrupt, chunk.descriptors);
                            continue;
                        }
                        if (chunk.kind === "reasoning") {
                            if (chunk.delta) {
                                yield (emit === null || emit === void 0 ? void 0 : emit({ type: "reasoning-delta", delta: chunk.delta }));
                            }
                            continue;
                        }
                        if (chunk.delta) {
                            fullResponse += chunk.delta;
                            yield textBuffer.push(chunk.delta, emit);
                        }
                    }
                }
                catch (e_1_1) { e_1 = { error: e_1_1 }; }
                finally {
                    try {
                        if (!_d && !_a && (_b = stream_1.return)) yield _b.call(stream_1);
                    }
                    finally { if (e_1) throw e_1.error; }
                }
                yield textBuffer.flush(emit);
                return { text: fullResponse };
            }
            finally {
                // When the turn is interrupted for HITL approval it will resume, so any steers
                // buffered meanwhile must survive to be applied on resume. Otherwise the turn is
                // done and leftover (unconsumed) steers must not leak into the next turn.
                if (!interrupted) {
                    this.deps.steerBuffer.clear(prepared.sessionId);
                }
                if (!this.deps.hasPersistentCheckpointer && !interrupted) {
                    this.pendingInterrupts.delete(prepared.sessionId);
                }
            }
        });
    }
    runAndPersistAgentResponse(input) {
        return __awaiter(this, void 0, void 0, function* () {
            var _a, _b, _c, _d;
            const prepared = yield this.prepareTurn(input);
            yield ((_b = (_a = prepared.observability).emit) === null || _b === void 0 ? void 0 : _b.call(_a, {
                type: "turn-persisted",
                sessionId: prepared.sessionId,
                turnId: prepared.turnId,
            }));
            let fullResponse = (_c = prepared.initialResponse) !== null && _c !== void 0 ? _c : "";
            let aborted = false;
            let failed = false;
            try {
                const agentResponse = yield this.runAgentTurn(prepared);
                fullResponse += agentResponse.text;
            }
            catch (error) {
                if (((_d = input.abortSignal) === null || _d === void 0 ? void 0 : _d.aborted) || isAbortError(error)) {
                    aborted = true;
                    logger.info(input.abortLogMessage);
                }
                else {
                    failed = true;
                    fullResponse = getErrorMessage(error);
                    logger.error(`${input.failureLogMessage}:\n${fullResponse}`);
                }
            }
            // Record the turn's tip checkpoint id ONLY on success, so message editing can
            // fork from it. On abort/failure leave the previous value untouched (undefined =
            // don't overwrite). Never let a checkpoint read break response persistence.
            let checkpointId;
            if (!aborted && !failed && this.deps.turnCheckpointsEnabled) {
                try {
                    checkpointId = yield this.deps.llm.getLatestCheckpointId({
                        completionAdapter: prepared.mode.completionAdapter,
                        sessionId: prepared.sessionId,
                    });
                }
                catch (error) {
                    logger.warn(`Failed to read latest checkpoint id for turn "${prepared.turnId}": ${getErrorMessage(error)}`);
                    checkpointId = undefined;
                }
            }
            prepared.observability.sequenceDebugSink.flush();
            yield this.deps.sessions.saveTurnResponse({
                turnId: prepared.turnId,
                responseText: fullResponse,
                debugHistory: prepared.observability.sequenceDebugSink.getHistory(),
                checkpointId,
            });
            return {
                text: fullResponse,
                turnId: prepared.turnId,
                aborted,
                failed,
            };
        });
    }
    handleTurn(input) {
        return __awaiter(this, void 0, void 0, function* () {
            var _a, _b;
            yield input.emit({
                type: "turn-started",
                messageId: randomUUID(),
            });
            const agentResponse = yield this.runAndPersistAgentResponse({
                prompt: input.prompt,
                sessionId: input.sessionId,
                modeName: input.modeName,
                userTimeZone: input.userTimeZone,
                currentPage: input.currentPage,
                chatSurface: input.chatSurface,
                adminPublicOrigin: input.adminPublicOrigin,
                approvalDecision: input.approvalDecision,
                abortSignal: input.abortSignal,
                adminUser: input.adminUser,
                emit: input.emit,
                failureLogMessage: (_a = input.failureLogMessage) !== null && _a !== void 0 ? _a : "Agent response failed",
                abortLogMessage: (_b = input.abortLogMessage) !== null && _b !== void 0 ? _b : "Agent response aborted",
            });
            if (agentResponse.failed) {
                yield input.emit({
                    type: "error",
                    error: agentResponse.text,
                });
            }
            else if (!agentResponse.aborted) {
                yield input.emit({
                    type: "response",
                    text: agentResponse.text,
                    sessionId: input.sessionId,
                    turnId: agentResponse.turnId,
                });
            }
            yield input.emit({
                type: "finish",
            });
            return agentResponse;
        });
    }
    handleEditTurn(input) {
        return __awaiter(this, void 0, void 0, function* () {
            var _a, _b;
            yield input.emit({
                type: "turn-started",
                messageId: randomUUID(),
            });
            const agentResponse = yield this.runAndPersistAgentResponse({
                prompt: input.prompt,
                sessionId: input.sessionId,
                modeName: input.modeName,
                userTimeZone: input.userTimeZone,
                currentPage: input.currentPage,
                chatSurface: input.chatSurface,
                adminPublicOrigin: input.adminPublicOrigin,
                editTurnId: input.turnId,
                abortSignal: input.abortSignal,
                adminUser: input.adminUser,
                emit: input.emit,
                failureLogMessage: (_a = input.failureLogMessage) !== null && _a !== void 0 ? _a : "Agent message edit failed",
                abortLogMessage: (_b = input.abortLogMessage) !== null && _b !== void 0 ? _b : "Agent message edit aborted",
            });
            if (agentResponse.failed) {
                yield input.emit({
                    type: "error",
                    error: agentResponse.text,
                });
            }
            else if (!agentResponse.aborted) {
                yield input.emit({
                    type: "response",
                    text: agentResponse.text,
                    sessionId: input.sessionId,
                    turnId: agentResponse.turnId,
                });
            }
            yield input.emit({
                type: "finish",
            });
            return agentResponse;
        });
    }
}
