"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MockScenarioPlayer = void 0;
const assert_1 = __importDefault(require("../../utils/assert"));
const message_1 = require("../../types/message");
const result_1 = require("../../types/result");
const log_1 = require("../../services/log");
const scenarios_1 = require("./scenarios");
const tokenizer_1 = require("../../utils/main/tokenizer");
const MOCK_TOKENIZER_MODEL = "openai:gpt-5";
const TOKENIZE_TIMEOUT_MS = 150;
let tokenizerFallbackLogged = false;
function approximateTokenCount(text) {
    const normalizedLength = text.trim().length;
    if (normalizedLength === 0) {
        return 0;
    }
    return Math.max(1, Math.ceil(normalizedLength / 4));
}
async function tokenizeWithMockModel(text, context) {
    (0, assert_1.default)(typeof text === "string", `Mock scenario ${context} expects string input`);
    const approximateTokens = approximateTokenCount(text);
    let fallbackUsed = false;
    let timeoutId;
    const fallbackPromise = new Promise((resolve) => {
        timeoutId = setTimeout(() => {
            fallbackUsed = true;
            resolve(approximateTokens);
        }, TOKENIZE_TIMEOUT_MS);
    });
    const actualPromise = (async () => {
        const tokenizer = await (0, tokenizer_1.getTokenizerForModel)(MOCK_TOKENIZER_MODEL);
        (0, assert_1.default)(typeof tokenizer.encoding === "string" && tokenizer.encoding.length > 0, `Tokenizer for ${MOCK_TOKENIZER_MODEL} must expose a non-empty encoding`);
        const tokens = await tokenizer.countTokens(text);
        (0, assert_1.default)(Number.isFinite(tokens) && tokens >= 0, `Tokenizer for ${MOCK_TOKENIZER_MODEL} returned invalid token count`);
        return tokens;
    })();
    let tokens;
    try {
        tokens = await Promise.race([actualPromise, fallbackPromise]);
    }
    catch (error) {
        if (timeoutId !== undefined) {
            clearTimeout(timeoutId);
        }
        const errorMessage = error instanceof Error ? error.message : String(error);
        throw new Error(`[MockScenarioPlayer] Failed to tokenize ${context} with ${MOCK_TOKENIZER_MODEL}: ${errorMessage}`);
    }
    if (!fallbackUsed && timeoutId !== undefined) {
        clearTimeout(timeoutId);
    }
    actualPromise
        .then((resolvedTokens) => {
        if (fallbackUsed && !tokenizerFallbackLogged) {
            tokenizerFallbackLogged = true;
            log_1.log.debug(`[MockScenarioPlayer] Tokenizer fallback used for ${context}; emitted ${approximateTokens}, background tokenizer returned ${resolvedTokens}`);
        }
    })
        .catch((error) => {
        if (fallbackUsed && !tokenizerFallbackLogged) {
            tokenizerFallbackLogged = true;
            const errorMessage = error instanceof Error ? error.message : String(error);
            log_1.log.debug(`[MockScenarioPlayer] Tokenizer fallback used for ${context}; background error: ${errorMessage}`);
        }
    });
    if (fallbackUsed) {
        (0, assert_1.default)(Number.isFinite(tokens) && tokens >= 0, `Token fallback produced invalid count for ${context}`);
    }
    return tokens;
}
class MockScenarioPlayer {
    deps;
    scenarios = scenarios_1.allScenarios;
    activeStreams = new Map();
    completedTurns = new Set();
    constructor(deps) {
        this.deps = deps;
    }
    isStreaming(workspaceId) {
        return this.activeStreams.has(workspaceId);
    }
    stop(workspaceId) {
        const active = this.activeStreams.get(workspaceId);
        if (!active)
            return;
        // Clear all pending timers
        for (const timer of active.timers) {
            clearTimeout(timer);
        }
        // Emit stream-abort event to mirror real streaming behavior
        this.deps.aiService.emit("stream-abort", {
            type: "stream-abort",
            workspaceId,
            messageId: active.messageId,
            reason: "user_cancelled",
        });
        this.activeStreams.delete(workspaceId);
    }
    async play(messages, workspaceId) {
        const latest = messages[messages.length - 1];
        if (!latest || latest.role !== "user") {
            return (0, result_1.Err)({ type: "unknown", raw: "Mock scenario expected a user message" });
        }
        const latestText = this.extractText(latest);
        const turnIndex = this.findTurnIndex(latestText);
        if (turnIndex === -1) {
            return (0, result_1.Err)({
                type: "unknown",
                raw: `Mock scenario turn mismatch. No scripted response for "${latestText}"`,
            });
        }
        const turn = this.scenarios[turnIndex];
        if (typeof turn.user.editOfTurn === "number" &&
            !this.completedTurns.has(turn.user.editOfTurn)) {
            return (0, result_1.Err)({
                type: "unknown",
                raw: `Mock scenario turn "${turn.user.text}" requires completion of turn index ${turn.user.editOfTurn}`,
            });
        }
        const streamStart = turn.assistant.events.find((event) => event.kind === "stream-start");
        if (!streamStart) {
            return (0, result_1.Err)({ type: "unknown", raw: "Mock scenario turn missing stream-start" });
        }
        let historySequence = this.computeNextHistorySequence(messages);
        const assistantMessage = (0, message_1.createCmuxMessage)(turn.assistant.messageId, "assistant", "", {
            timestamp: Date.now(),
            model: streamStart.model,
        });
        const appendResult = await this.deps.historyService.appendToHistory(workspaceId, assistantMessage);
        if (!appendResult.success) {
            return (0, result_1.Err)({ type: "unknown", raw: appendResult.error });
        }
        historySequence = assistantMessage.metadata?.historySequence ?? historySequence;
        // Cancel any existing stream before starting a new one
        if (this.isStreaming(workspaceId)) {
            this.stop(workspaceId);
        }
        this.scheduleEvents(workspaceId, turn, historySequence);
        this.completedTurns.add(turnIndex);
        return (0, result_1.Ok)(undefined);
    }
    async replayStream(_workspaceId) {
        // No-op for mock scenario; events are deterministic and do not support mid-stream replay
    }
    scheduleEvents(workspaceId, turn, historySequence) {
        const timers = [];
        this.activeStreams.set(workspaceId, {
            timers,
            messageId: turn.assistant.messageId,
            eventQueue: [],
            isProcessing: false,
        });
        for (const event of turn.assistant.events) {
            const timer = setTimeout(() => {
                this.enqueueEvent(workspaceId, () => this.dispatchEvent(workspaceId, event, turn.assistant.messageId, historySequence));
            }, event.delay);
            timers.push(timer);
        }
    }
    enqueueEvent(workspaceId, handler) {
        const active = this.activeStreams.get(workspaceId);
        if (!active)
            return;
        active.eventQueue.push(handler);
        void this.processQueue(workspaceId);
    }
    async processQueue(workspaceId) {
        const active = this.activeStreams.get(workspaceId);
        if (!active || active.isProcessing)
            return;
        active.isProcessing = true;
        while (active.eventQueue.length > 0) {
            const handler = active.eventQueue.shift();
            if (!handler)
                break;
            try {
                await handler();
            }
            catch (error) {
                console.error(`[MockScenarioPlayer] Event handler error for ${workspaceId}:`, error);
            }
        }
        active.isProcessing = false;
    }
    async dispatchEvent(workspaceId, event, messageId, historySequence) {
        switch (event.kind) {
            case "stream-start": {
                const payload = {
                    type: "stream-start",
                    workspaceId,
                    messageId,
                    model: event.model,
                    historySequence,
                };
                this.deps.aiService.emit("stream-start", payload);
                break;
            }
            case "reasoning-delta": {
                // Mock scenarios use the same tokenization logic as real streams for consistency
                const tokens = await tokenizeWithMockModel(event.text, "reasoning-delta text");
                const payload = {
                    type: "reasoning-delta",
                    workspaceId,
                    messageId,
                    delta: event.text,
                    tokens,
                    timestamp: Date.now(),
                };
                this.deps.aiService.emit("reasoning-delta", payload);
                break;
            }
            case "tool-start": {
                // Mock scenarios use the same tokenization logic as real streams for consistency
                const inputText = JSON.stringify(event.args);
                const tokens = await tokenizeWithMockModel(inputText, "tool-call args");
                const payload = {
                    type: "tool-call-start",
                    workspaceId,
                    messageId,
                    toolCallId: event.toolCallId,
                    toolName: event.toolName,
                    args: event.args,
                    tokens,
                    timestamp: Date.now(),
                };
                this.deps.aiService.emit("tool-call-start", payload);
                break;
            }
            case "tool-end": {
                const payload = {
                    type: "tool-call-end",
                    workspaceId,
                    messageId,
                    toolCallId: event.toolCallId,
                    toolName: event.toolName,
                    result: event.result,
                };
                this.deps.aiService.emit("tool-call-end", payload);
                break;
            }
            case "stream-delta": {
                // Mock scenarios use the same tokenization logic as real streams for consistency
                let tokens;
                try {
                    tokens = await tokenizeWithMockModel(event.text, "stream-delta text");
                }
                catch (error) {
                    console.error("[MockScenarioPlayer] tokenize failed for stream-delta", error);
                    throw error;
                }
                const payload = {
                    type: "stream-delta",
                    workspaceId,
                    messageId,
                    delta: event.text,
                    tokens,
                    timestamp: Date.now(),
                };
                this.deps.aiService.emit("stream-delta", payload);
                break;
            }
            case "stream-error": {
                const payload = event;
                this.deps.aiService.emit("error", {
                    type: "error",
                    workspaceId,
                    messageId,
                    error: payload.error,
                    errorType: payload.errorType,
                });
                this.cleanup(workspaceId);
                break;
            }
            case "stream-end": {
                const payload = {
                    type: "stream-end",
                    workspaceId,
                    messageId,
                    metadata: {
                        model: event.metadata.model,
                        systemMessageTokens: event.metadata.systemMessageTokens,
                    },
                    parts: event.parts,
                };
                // Update history with completed message (mirrors real StreamManager behavior)
                // Fetch the current message from history to get its historySequence
                const historyResult = await this.deps.historyService.getHistory(workspaceId);
                if (historyResult.success) {
                    const existingMessage = historyResult.data.find((msg) => msg.id === messageId);
                    if (existingMessage?.metadata?.historySequence !== undefined) {
                        const completedMessage = {
                            id: messageId,
                            role: "assistant",
                            parts: event.parts,
                            metadata: {
                                ...existingMessage.metadata,
                                model: event.metadata.model,
                                systemMessageTokens: event.metadata.systemMessageTokens,
                            },
                        };
                        const updateResult = await this.deps.historyService.updateHistory(workspaceId, completedMessage);
                        if (!updateResult.success) {
                            console.error(`Failed to update history for ${messageId}: ${updateResult.error}`);
                        }
                    }
                }
                this.deps.aiService.emit("stream-end", payload);
                this.cleanup(workspaceId);
                break;
            }
        }
    }
    cleanup(workspaceId) {
        const active = this.activeStreams.get(workspaceId);
        if (!active)
            return;
        // Clear all pending timers
        for (const timer of active.timers) {
            clearTimeout(timer);
        }
        // Clear event queue to prevent any pending events from processing
        active.eventQueue = [];
        this.activeStreams.delete(workspaceId);
    }
    extractText(message) {
        return message.parts
            .filter((part) => "text" in part)
            .map((part) => part.text)
            .join("");
    }
    computeNextHistorySequence(messages) {
        let maxSequence = 0;
        for (const message of messages) {
            const seq = message.metadata?.historySequence;
            if (typeof seq === "number" && seq > maxSequence) {
                maxSequence = seq;
            }
        }
        return maxSequence + 1;
    }
    findTurnIndex(text) {
        const normalizedText = text.trim();
        for (let index = 0; index < this.scenarios.length; index += 1) {
            if (this.completedTurns.has(index)) {
                continue;
            }
            const candidate = this.scenarios[index];
            if (candidate.user.text.trim() === normalizedText) {
                return index;
            }
        }
        return -1;
    }
}
exports.MockScenarioPlayer = MockScenarioPlayer;
//# sourceMappingURL=mockScenarioPlayer.js.map