import { Codex, } from "@openai/codex-sdk";
import { resolveCodexCli } from "./codex-cli.js";
import { normalizeCodexServiceTier, readCodexFastMode, writeCodexFastMode } from "./codex-config.js";
import { getThread, getThreadRolloutSnapshot, getThreadUsage, listModels, listThreads, listWorkspaces, } from "./codex-state.js";
import { createLaunchProfile, findLaunchProfile, formatLaunchProfileBehavior, } from "./codex-launch.js";
import { CODEX_AGENT_CAPABILITIES, } from "../shared/agent.js";
export class CodexSessionService {
    config;
    codex = null;
    thread = null;
    currentWorkspace;
    abortController = null;
    currentThreadId = null;
    currentModel;
    currentReasoningEffort;
    currentLaunchProfile;
    activeThreadLaunchProfile = null;
    activeThreadLaunchProfileOverride = null;
    sessionTokens = { input: 0, cached: 0, output: 0 };
    lastObservedFastMode = null;
    constructor(config) {
        this.config = config;
        this.currentWorkspace = config.workspace;
        this.currentLaunchProfile = getLaunchProfile(config, config.defaultLaunchProfileId);
    }
    static async create(config, options) {
        const service = new CodexSessionService(config);
        service.currentWorkspace = options?.workspace ?? config.workspace;
        service.currentModel = options?.model ?? config.codexModel;
        service.currentReasoningEffort = options?.reasoningEffort;
        service.currentLaunchProfile = getLaunchProfile(config, options?.launchProfileId ?? config.defaultLaunchProfileId);
        service.resetCodexClient();
        if (options?.resumeThreadId) {
            if (options.activeLaunchProfileId) {
                service.activeThreadLaunchProfileOverride = {
                    threadId: options.resumeThreadId,
                    profile: getLaunchProfile(config, options.activeLaunchProfileId),
                };
            }
            await service.resumeThread(options.resumeThreadId);
            return service;
        }
        if (options?.deferThreadStart) {
            return service;
        }
        await service.newThread(service.currentWorkspace, service.currentModel);
        return service;
    }
    getInfo(options = {}) {
        const activeThreadId = this.thread?.id ?? this.currentThreadId;
        if (activeThreadId && !this.abortController) {
            this.refreshActiveThreadMetadata(activeThreadId);
        }
        const effectiveLaunchProfile = this.activeThreadLaunchProfile ?? this.currentLaunchProfile;
        const codexFastMode = readCodexFastMode();
        const effectiveFastMode = codexFastMode ?? this.lastObservedFastMode ?? false;
        this.lastObservedFastMode = effectiveFastMode;
        const info = {
            agentId: "codex",
            agentLabel: "Codex",
            threadId: activeThreadId,
            workspace: this.currentWorkspace,
            model: this.currentModel ?? this.config.codexModel,
            launchProfileId: effectiveLaunchProfile.id,
            launchProfileLabel: effectiveLaunchProfile.label,
            launchProfileBehavior: formatLaunchProfileBehavior(effectiveLaunchProfile),
            sandboxMode: effectiveLaunchProfile.sandboxMode,
            approvalPolicy: effectiveLaunchProfile.approvalPolicy,
            fastMode: effectiveFastMode,
            unsafeLaunch: effectiveLaunchProfile.unsafe,
            capabilities: CODEX_AGENT_CAPABILITIES,
        };
        Object.defineProperties(info, {
            agentId: { value: "codex", enumerable: false },
            agentLabel: { value: "Codex", enumerable: false },
            capabilities: { value: CODEX_AGENT_CAPABILITIES, enumerable: false },
        });
        if (this.currentReasoningEffort) {
            info.reasoningEffort = this.currentReasoningEffort;
        }
        if (this.activeThreadLaunchProfile &&
            this.activeThreadLaunchProfile.id !== this.currentLaunchProfile.id) {
            info.nextLaunchProfileId = this.currentLaunchProfile.id;
            info.nextLaunchProfileLabel = this.currentLaunchProfile.label;
            info.nextLaunchProfileBehavior = formatLaunchProfileBehavior(this.currentLaunchProfile);
            info.nextUnsafeLaunch = this.currentLaunchProfile.unsafe;
        }
        if (this.sessionTokens.input > 0 || this.sessionTokens.cached > 0 || this.sessionTokens.output > 0) {
            info.sessionTokens = { ...this.sessionTokens };
        }
        const threadId = info.threadId;
        if (options.includeUsage && threadId) {
            const codexUsage = getThreadUsage(threadId);
            if (codexUsage) {
                info.codexUsage = codexUsage;
            }
        }
        return info;
    }
    isProcessing() {
        return this.abortController !== null;
    }
    getActiveThreadId() {
        return this.thread?.id ?? this.currentThreadId;
    }
    hasActiveThread() {
        return this.thread !== null;
    }
    getCurrentWorkspace() {
        return this.currentWorkspace;
    }
    async prompt(input, callbacks) {
        if (!this.thread) {
            throw new Error("Codex thread is not initialized");
        }
        if (this.abortController) {
            throw new Error("A Codex turn is already in progress");
        }
        const controller = new AbortController();
        this.abortController = controller;
        const activeThread = this.thread;
        let lastAgentText = "";
        // Track cumulative aggregated_output per command item to compute deltas.
        const lastCommandOutput = new Map();
        try {
            const { events } = await activeThread.runStreamed(this.buildSdkInput(input), { signal: controller.signal });
            for await (const event of events) {
                if (this.thread === activeThread) {
                    this.handleThreadEvent(event);
                }
                switch (event.type) {
                    case "item.started":
                    case "item.updated": {
                        const item = event.item;
                        if (item.type === "agent_message") {
                            const delta = computeTextDelta(lastAgentText, item.text);
                            if (delta) {
                                lastAgentText = item.text;
                                callbacks.onTextDelta(delta);
                            }
                            else {
                                lastAgentText = item.text;
                            }
                        }
                        else if (item.type === "command_execution") {
                            if (event.type === "item.started") {
                                // Record baseline so the first item.updated delta is computed correctly.
                                lastCommandOutput.set(item.id, item.aggregated_output);
                                callbacks.onToolStart(item.command, item.id);
                            }
                            else {
                                // aggregated_output grows monotonically; pass only the new portion.
                                const prev = lastCommandOutput.get(item.id) ?? "";
                                const delta = computeTextDelta(prev, item.aggregated_output);
                                lastCommandOutput.set(item.id, item.aggregated_output);
                                if (delta) {
                                    callbacks.onToolUpdate(item.id, delta);
                                }
                            }
                        }
                        else if (item.type === "web_search") {
                            if (event.type === "item.started") {
                                const label = truncate(item.query, 60);
                                callbacks.onToolStart(`🔍 ${label}`, item.id);
                                callbacks.onToolUpdate(item.id, item.query);
                            }
                        }
                        else if (item.type === "todo_list") {
                            callbacks.onTodoUpdate?.(item.items);
                        }
                        break;
                    }
                    case "item.completed": {
                        const item = event.item;
                        if (item.type === "agent_message") {
                            const delta = computeTextDelta(lastAgentText, item.text);
                            if (delta) {
                                callbacks.onTextDelta(delta);
                            }
                            lastAgentText = item.text;
                            if (item.text.trim()) {
                                callbacks.onAssistantMessageComplete?.();
                            }
                        }
                        else if (item.type === "command_execution") {
                            // Pass any output that arrived only in the completion event (e.g. fast
                            // commands that never fired item.updated).
                            const prev = lastCommandOutput.get(item.id) ?? "";
                            const delta = computeTextDelta(prev, item.aggregated_output);
                            if (delta) {
                                callbacks.onToolUpdate(item.id, delta);
                            }
                            callbacks.onToolEnd(item.id, item.status === "failed");
                        }
                        else if (item.type === "file_change") {
                            const toolId = item.id;
                            const summary = item.changes.map((change) => `${change.kind} ${change.path}`).join(", ");
                            callbacks.onToolStart("file_change", toolId);
                            callbacks.onToolUpdate(toolId, summary);
                            callbacks.onToolEnd(toolId, item.status === "failed");
                        }
                        else if (item.type === "mcp_tool_call") {
                            callbacks.onToolStart(`mcp:${item.server}/${item.tool}`, item.id);
                            if (item.error) {
                                callbacks.onToolUpdate(item.id, item.error.message);
                            }
                            callbacks.onToolEnd(item.id, item.status === "failed");
                        }
                        else if (item.type === "web_search") {
                            callbacks.onToolEnd(item.id, false);
                        }
                        else if (item.type === "error") {
                            callbacks.onToolStart("⚠️ error", item.id);
                            callbacks.onToolUpdate(item.id, item.message);
                            callbacks.onToolEnd(item.id, true);
                        }
                        else if (item.type === "todo_list") {
                            callbacks.onTodoUpdate?.(item.items);
                        }
                        break;
                    }
                    case "turn.completed": {
                        // Accumulate and deliver usage BEFORE onAgentEnd so that
                        // finalizeResponse() can read lastTurnUsage when building the
                        // final message text.
                        const u = event.usage;
                        this.sessionTokens.input += u.input_tokens;
                        this.sessionTokens.cached += u.cached_input_tokens;
                        this.sessionTokens.output += u.output_tokens;
                        callbacks.onTurnComplete?.({
                            inputTokens: u.input_tokens,
                            cachedInputTokens: u.cached_input_tokens,
                            outputTokens: u.output_tokens,
                        });
                        callbacks.onAgentEnd();
                        break;
                    }
                    case "turn.failed":
                        throw new Error(event.error.message);
                    case "error":
                        throw new Error(event.message);
                    default:
                        break;
                }
            }
        }
        finally {
            if (this.abortController === controller) {
                this.abortController = null;
            }
        }
    }
    async abort() {
        this.abortController?.abort();
    }
    async newThread(workspace, model) {
        this.ensureIdle("start a new thread");
        const effectiveWorkspace = workspace ?? this.currentWorkspace;
        const effectiveModel = model ?? this.currentModel;
        this.thread = this.getCodex().startThread(this.buildThreadOptions(effectiveWorkspace, effectiveModel));
        this.activeThreadLaunchProfile = this.currentLaunchProfile;
        this.activeThreadLaunchProfileOverride = null;
        this.currentWorkspace = effectiveWorkspace;
        this.currentThreadId = this.thread.id ?? null;
        if (model) {
            this.currentModel = model;
        }
        return this.getInfo();
    }
    async resumeThread(threadId) {
        this.ensureIdle("resume a thread");
        const record = getThread(threadId);
        const workspace = record?.cwd ?? this.currentWorkspace;
        const model = record?.model || this.currentModel;
        const launchProfile = this.launchProfileOverrideFor(threadId) ?? this.resolveThreadLaunchProfile(record);
        if (record) {
            this.currentReasoningEffort = record.reasoningEffort
                ? record.reasoningEffort
                : undefined;
        }
        this.thread = this.getCodex().resumeThread(threadId, this.buildThreadOptions(workspace, model, launchProfile));
        this.activeThreadLaunchProfile = launchProfile;
        this.currentThreadId = threadId;
        this.currentWorkspace = workspace;
        if (model) {
            this.currentModel = model;
        }
        return this.getInfo();
    }
    async switchSession(threadId) {
        const record = getThread(threadId);
        const workspace = record?.cwd ?? this.currentWorkspace;
        const model = record?.model || undefined;
        const reasoningEffort = record?.reasoningEffort || undefined;
        const launchProfile = this.resolveThreadLaunchProfile(record);
        this.currentReasoningEffort = reasoningEffort;
        this.thread = this.getCodex().resumeThread(threadId, this.buildThreadOptions(workspace, model, launchProfile));
        this.activeThreadLaunchProfile = launchProfile;
        this.activeThreadLaunchProfileOverride = null;
        this.currentWorkspace = workspace;
        this.currentThreadId = threadId;
        if (model) {
            this.currentModel = model;
        }
        return this.getInfo();
    }
    listAllSessions(limit) {
        return listThreads(limit ?? 20).map(toAgentThreadRecord);
    }
    listWorkspaces() {
        return listWorkspaces();
    }
    async refreshModels() {
        // Codex models are read from local state on each listModels() call.
    }
    listModels() {
        return listModels();
    }
    listLaunchProfiles() {
        return this.config.launchProfiles.map((profile) => ({
            id: profile.id,
            label: profile.label,
            behavior: formatLaunchProfileBehavior(profile),
            unsafe: profile.unsafe,
        }));
    }
    getSessionRecord(threadId) {
        const record = getThread(threadId);
        return record ? toAgentThreadRecord(record) : null;
    }
    setModel(slug) {
        this.currentModel = slug;
        return slug;
    }
    setModelForCurrentSession(slug) {
        this.ensureIdle("change model");
        this.currentModel = slug;
        const appliedToActiveThread = this.reattachActiveThread();
        return { value: slug, appliedToActiveThread };
    }
    setReasoningEffort(effort) {
        this.currentReasoningEffort = effort;
    }
    setReasoningEffortForCurrentSession(effort) {
        this.ensureIdle("change reasoning effort");
        this.currentReasoningEffort = effort;
        const appliedToActiveThread = this.reattachActiveThread();
        return { value: effort, appliedToActiveThread };
    }
    setLaunchProfile(profileId) {
        this.currentLaunchProfile = getLaunchProfile(this.config, profileId);
        this.resetCodexClient();
        return this.currentLaunchProfile;
    }
    setLaunchProfileForCurrentSession(profileId) {
        this.ensureIdle("change launch profile");
        const profile = getLaunchProfile(this.config, profileId);
        this.currentLaunchProfile = profile;
        this.resetCodexClient();
        const appliedToActiveThread = this.reattachActiveThread(profile);
        if (appliedToActiveThread && this.currentThreadId) {
            this.activeThreadLaunchProfileOverride = { threadId: this.currentThreadId, profile };
        }
        return { value: profile.id, appliedToActiveThread };
    }
    setFastMode(enabled) {
        this.ensureIdle("change fast mode");
        writeCodexFastMode(enabled);
        this.lastObservedFastMode = enabled;
        this.resetCodexClient();
        const profile = this.activeThreadLaunchProfile ?? this.currentLaunchProfile;
        let appliedToActiveThread = false;
        if (this.thread) {
            if (this.currentThreadId) {
                this.thread = this.getCodex().resumeThread(this.currentThreadId, this.buildThreadOptions(this.currentWorkspace, this.currentModel, profile));
            }
            else {
                this.thread = this.getCodex().startThread(this.buildThreadOptions(this.currentWorkspace, this.currentModel, profile));
            }
            this.activeThreadLaunchProfile = profile;
            if (this.currentThreadId) {
                this.activeThreadLaunchProfileOverride = { threadId: this.currentThreadId, profile };
            }
            appliedToActiveThread = true;
        }
        return { enabled, profile, appliedToActiveThread };
    }
    getSelectedLaunchProfile() {
        return this.currentLaunchProfile;
    }
    syncFromAgentState(options = {}) {
        const activeThreadId = this.thread?.id ?? this.currentThreadId;
        const before = {
            workspace: this.currentWorkspace,
            model: this.currentModel,
            reasoningEffort: this.currentReasoningEffort,
            activeLaunchProfileId: this.activeThreadLaunchProfile?.id,
            selectedLaunchProfileId: this.currentLaunchProfile.id,
        };
        const changedFields = new Set();
        if (activeThreadId && !this.abortController) {
            const record = getThread(activeThreadId);
            if (record) {
                if (record.cwd && record.cwd !== this.currentWorkspace)
                    changedFields.add("workspace");
                if ((record.model || undefined) !== this.currentModel)
                    changedFields.add("model");
                if ((record.reasoningEffort || undefined) !== this.currentReasoningEffort)
                    changedFields.add("reasoning");
                const resolvedLaunchProfile = this.launchProfileOverrideFor(activeThreadId) ?? this.resolveThreadLaunchProfile(record);
                if (resolvedLaunchProfile.id !== this.activeThreadLaunchProfile?.id)
                    changedFields.add("launch");
                this.currentWorkspace = record.cwd || this.currentWorkspace;
                this.currentModel = record.model || this.currentModel;
                this.currentReasoningEffort = record.reasoningEffort
                    ? record.reasoningEffort
                    : undefined;
                this.activeThreadLaunchProfile = resolvedLaunchProfile;
            }
        }
        const codexFastMode = readCodexFastMode();
        if (codexFastMode !== null && codexFastMode !== this.lastObservedFastMode) {
            changedFields.add("fast");
            this.lastObservedFastMode = codexFastMode;
        }
        const changed = changedFields.size > 0 ||
            before.workspace !== this.currentWorkspace ||
            before.model !== this.currentModel ||
            before.reasoningEffort !== this.currentReasoningEffort ||
            before.activeLaunchProfileId !== this.activeThreadLaunchProfile?.id ||
            before.selectedLaunchProfileId !== this.currentLaunchProfile.id;
        let reattached = false;
        if (changed && options.reattach && !this.abortController && this.thread) {
            reattached = this.reattachActiveThread();
        }
        return {
            threadId: activeThreadId,
            changed,
            reattached,
            changedFields: [...changedFields],
            info: this.getInfo(),
        };
    }
    handback() {
        const info = { threadId: this.currentThreadId, workspace: this.currentWorkspace };
        this.abortController?.abort();
        this.abortController = null;
        this.thread = null;
        this.currentThreadId = null;
        this.activeThreadLaunchProfile = null;
        this.activeThreadLaunchProfileOverride = null;
        return info;
    }
    dispose() {
        this.abortController?.abort();
        this.abortController = null;
        this.thread = null;
        this.currentThreadId = null;
        this.activeThreadLaunchProfile = null;
        this.activeThreadLaunchProfileOverride = null;
    }
    buildSdkInput(input) {
        if (typeof input === "string") {
            return input;
        }
        const parts = [];
        const textParts = [];
        if (input.stagedFileInstructions) {
            textParts.push(input.stagedFileInstructions);
        }
        if (input.text) {
            textParts.push(input.text);
        }
        if (textParts.length > 0) {
            parts.push({ type: "text", text: textParts.join("\n\n") });
        }
        for (const imagePath of input.imagePaths ?? []) {
            parts.push({ type: "local_image", path: imagePath });
        }
        if (parts.length === 0) {
            return "";
        }
        if (parts.length === 1 && parts[0]?.type === "text") {
            return parts[0].text;
        }
        return parts;
    }
    buildThreadOptions(workspace, model, launchProfile = this.currentLaunchProfile) {
        const effectiveModel = model ?? this.currentModel ?? this.config.codexModel;
        const options = {
            model: effectiveModel,
            sandboxMode: launchProfile.sandboxMode,
            workingDirectory: workspace,
            approvalPolicy: launchProfile.approvalPolicy,
            skipGitRepoCheck: true,
        };
        if (this.currentReasoningEffort) {
            return {
                ...options,
                modelReasoningEffort: this.currentReasoningEffort,
            };
        }
        return options;
    }
    ensureIdle(action) {
        if (this.abortController) {
            throw new Error(`Cannot ${action} while a turn is in progress`);
        }
    }
    handleThreadEvent(event) {
        if (event.type === "thread.started") {
            this.currentThreadId = event.thread_id;
        }
    }
    getCodex() {
        if (!this.codex) {
            this.resetCodexClient();
        }
        return this.codex;
    }
    resetCodexClient() {
        normalizeCodexServiceTier();
        const cli = resolveCodexCli();
        const options = {
            apiKey: this.config.codexApiKey,
            config: {
                approval_policy: this.currentLaunchProfile.approvalPolicy,
            },
            env: buildCodexEnv(this.config.codexApiKey),
        };
        if (cli.path) {
            options.codexPathOverride = cli.path;
        }
        this.codex = new Codex(options);
    }
    resolveThreadLaunchProfile(record) {
        const rolloutSnapshot = record?.id
            ? getThreadRolloutSnapshot(record.id, { maxEvents: 0 })
            : null;
        const sandboxMode = rolloutSnapshot?.sandboxMode ?? record?.sandboxMode ?? null;
        const approvalPolicy = rolloutSnapshot?.approvalPolicy ?? record?.approvalPolicy ?? null;
        if (!sandboxMode || !approvalPolicy) {
            return this.currentLaunchProfile;
        }
        const matchingProfile = this.config.launchProfiles.find((profile) => profile.sandboxMode === sandboxMode && profile.approvalPolicy === approvalPolicy);
        if (matchingProfile) {
            return matchingProfile;
        }
        return createLaunchProfile({
            id: "attached-thread",
            label: "Attached Thread",
            sandboxMode,
            approvalPolicy,
        });
    }
    refreshActiveThreadMetadata(threadId) {
        const record = getThread(threadId);
        if (!record) {
            return;
        }
        this.currentWorkspace = record.cwd || this.currentWorkspace;
        this.currentModel = record.model || this.currentModel;
        this.currentReasoningEffort = record.reasoningEffort
            ? record.reasoningEffort
            : undefined;
        this.activeThreadLaunchProfile = this.launchProfileOverrideFor(threadId) ?? this.resolveThreadLaunchProfile(record);
    }
    reattachActiveThread(launchProfileOverride) {
        if (!this.thread) {
            this.resetCodexClient();
            return false;
        }
        const launchProfile = launchProfileOverride ?? this.activeThreadLaunchProfile ?? this.currentLaunchProfile;
        if (this.currentThreadId) {
            this.thread = this.getCodex().resumeThread(this.currentThreadId, this.buildThreadOptions(this.currentWorkspace, this.currentModel, launchProfile));
        }
        else {
            this.thread = this.getCodex().startThread(this.buildThreadOptions(this.currentWorkspace, this.currentModel, launchProfile));
            this.currentThreadId = this.thread.id ?? null;
        }
        this.activeThreadLaunchProfile = launchProfile;
        return true;
    }
    launchProfileOverrideFor(threadId) {
        if (!threadId || this.activeThreadLaunchProfileOverride?.threadId !== threadId) {
            return null;
        }
        return this.activeThreadLaunchProfileOverride.profile;
    }
}
function getLaunchProfile(config, profileId) {
    const profile = findLaunchProfile(config.launchProfiles, profileId);
    if (!profile) {
        if (profileId === "full-access") {
            return createLaunchProfile({
                id: "full-access",
                label: "Full Access",
                sandboxMode: "danger-full-access",
                approvalPolicy: "never",
            });
        }
        throw new Error(`Unknown launch profile: ${profileId}`);
    }
    return profile;
}
function toAgentThreadRecord(record) {
    return {
        ...record,
        agentId: "codex",
    };
}
function buildCodexEnv(apiKey) {
    const env = {};
    for (const [key, value] of Object.entries(process.env)) {
        if (value !== undefined) {
            env[key] = value;
        }
    }
    if (apiKey) {
        env.CODEX_API_KEY = apiKey;
    }
    return env;
}
function computeTextDelta(previousText, nextText) {
    return nextText.startsWith(previousText) ? nextText.slice(previousText.length) : nextText;
}
function truncate(text, maxLength) {
    return text.length <= maxLength ? text : `${text.slice(0, maxLength - 1)}…`;
}
