/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { getResponseText, getFunctionCalls, } from '../utils/generateContentResponseUtilities.js';
import { reportError } from '../utils/errorReporting.js';
import { getErrorMessage, UnauthorizedError, toFriendlyError, } from '../utils/errors.js';
import { normalizeToolName } from '../tools/toolNameUtils.js';
import { InvalidStreamError } from './geminiChat.js';
import { DebugLogger } from '../debug/index.js';
import { getCodeAssistServer } from '../code_assist/codeAssist.js';
import { UserTierId } from '../code_assist/types.js';
import { parseThought } from '../utils/thoughtUtils.js';
export const DEFAULT_AGENT_ID = 'primary';
export var GeminiEventType;
(function (GeminiEventType) {
    GeminiEventType["Content"] = "content";
    GeminiEventType["ToolCallRequest"] = "tool_call_request";
    GeminiEventType["ToolCallResponse"] = "tool_call_response";
    GeminiEventType["ToolCallConfirmation"] = "tool_call_confirmation";
    GeminiEventType["UserCancelled"] = "user_cancelled";
    GeminiEventType["Error"] = "error";
    GeminiEventType["ChatCompressed"] = "chat_compressed";
    GeminiEventType["Thought"] = "thought";
    GeminiEventType["UsageMetadata"] = "usage_metadata";
    GeminiEventType["MaxSessionTurns"] = "max_session_turns";
    GeminiEventType["Finished"] = "finished";
    GeminiEventType["LoopDetected"] = "loop_detected";
    GeminiEventType["Citation"] = "citation";
    GeminiEventType["Retry"] = "retry";
    GeminiEventType["SystemNotice"] = "system_notice";
    GeminiEventType["InvalidStream"] = "invalid_stream";
})(GeminiEventType || (GeminiEventType = {}));
export var CompressionStatus;
(function (CompressionStatus) {
    /** The compression was successful */
    CompressionStatus[CompressionStatus["COMPRESSED"] = 1] = "COMPRESSED";
    /** The compression failed due to the compression inflating the token count */
    CompressionStatus[CompressionStatus["COMPRESSION_FAILED_INFLATED_TOKEN_COUNT"] = 2] = "COMPRESSION_FAILED_INFLATED_TOKEN_COUNT";
    /** The compression failed due to an error counting tokens */
    CompressionStatus[CompressionStatus["COMPRESSION_FAILED_TOKEN_COUNT_ERROR"] = 3] = "COMPRESSION_FAILED_TOKEN_COUNT_ERROR";
    /** The compression was not necessary and no action was taken */
    CompressionStatus[CompressionStatus["NOOP"] = 4] = "NOOP";
})(CompressionStatus || (CompressionStatus = {}));
// A turn manages the agentic loop turn within the server context.
export class Turn {
    chat;
    prompt_id;
    agentId;
    providerName;
    pendingToolCalls;
    debugResponses;
    finishReason;
    logger;
    constructor(chat, prompt_id, agentId = DEFAULT_AGENT_ID, providerName = 'backend') {
        this.chat = chat;
        this.prompt_id = prompt_id;
        this.agentId = agentId;
        this.providerName = providerName;
        this.pendingToolCalls = [];
        this.debugResponses = [];
        this.finishReason = undefined;
        this.logger = new DebugLogger('llxprt:core:turn');
    }
    /**
     * Check if citations should be shown for the current user/settings.
     * Based on the upstream implementation from commit 997136ae.
     */
    shouldShowCitations() {
        try {
            // Access config through the chat instance
            const config = this.chat.config;
            const settingsService = config?.getSettingsService();
            if (settingsService) {
                const enabled = settingsService.get('ui.showCitations');
                if (enabled !== undefined) {
                    return enabled;
                }
            }
            // Fallback: check user tier for code assist server
            const server = getCodeAssistServer(config);
            return (server && server.userTier !== UserTierId.FREE) ?? false;
        }
        catch (error) {
            this.logger.debug('Failed to determine citation settings:', error);
            return false;
        }
    }
    /**
     * Emits a citation event with the given text.
     * This integrates with llxprt's provider abstraction to work across all providers.
     */
    emitCitation(text) {
        if (!this.shouldShowCitations()) {
            return null;
        }
        return {
            type: GeminiEventType.Citation,
            value: text,
        };
    }
    // The run method yields simpler events suitable for server logic
    async *run(req, signal) {
        this.logger.debug('Turn.run called', {
            req: JSON.stringify(req, null, 2),
            typeofReq: typeof req,
            isArray: Array.isArray(req),
        });
        try {
            // Note: This assumes `sendMessageStream` yields events like
            // { type: StreamEventType.RETRY } or { type: StreamEventType.CHUNK, value: GenerateContentResponse }
            const responseStream = await this.chat.sendMessageStream({
                message: req,
                config: {
                    abortSignal: signal,
                },
            }, this.prompt_id);
            for await (const streamEvent of responseStream) {
                if (signal?.aborted) {
                    yield { type: GeminiEventType.UserCancelled };
                    return;
                }
                // Handle the new RETRY event
                if (streamEvent.type === 'retry') {
                    yield { type: GeminiEventType.Retry };
                    continue; // Skip to the next event in the stream
                }
                // Assuming other events are chunks with a `value` property
                const resp = streamEvent.value;
                if (!resp)
                    continue; // Skip if there's no response body
                this.debugResponses.push(resp);
                // Check ALL parts for thinking, not just parts[0]
                // Bug fix: Previously only checked parts[0], missing thoughts in other positions
                // @plan PLAN-20251202-THINKING.P16
                const allParts = resp.candidates?.[0]?.content?.parts ?? [];
                for (const part of allParts) {
                    if (part.thought) {
                        const thought = parseThought(part.text ?? '');
                        yield {
                            type: GeminiEventType.Thought,
                            value: thought,
                        };
                    }
                }
                const text = getResponseText(resp);
                if (text) {
                    yield { type: GeminiEventType.Content, value: text };
                    // Emit citation event if conditions are met
                    // Based on upstream implementation - emit citation after content
                    const citationEvent = this.emitCitation('Response may contain information from external sources. Please verify important details independently.');
                    if (citationEvent) {
                        yield citationEvent;
                    }
                }
                // Handle function calls (requesting tool execution)
                const functionCalls = getFunctionCalls(resp) ?? [];
                for (const fnCall of functionCalls) {
                    const event = this.handlePendingFunctionCall(fnCall);
                    if (event) {
                        yield event;
                    }
                }
                // Check if response was truncated or stopped for various reasons
                const finishReason = resp.candidates?.[0]?.finishReason;
                // This is the key change: Only yield 'Finished' if there is a finishReason.
                if (finishReason) {
                    this.finishReason = finishReason;
                    yield {
                        type: GeminiEventType.Finished,
                        value: {
                            reason: finishReason,
                            usageMetadata: resp.usageMetadata,
                        },
                    };
                }
            }
        }
        catch (e) {
            if (signal.aborted) {
                yield { type: GeminiEventType.UserCancelled };
                // Regular cancellation error, fail gracefully.
                return;
            }
            if (e instanceof InvalidStreamError) {
                yield { type: GeminiEventType.InvalidStream };
                return;
            }
            const error = toFriendlyError(e);
            if (error instanceof UnauthorizedError) {
                throw error;
            }
            const contextForReport = [...this.chat.getHistory(/*curated*/ true), req];
            await reportError(error, `Error when talking to ${this.providerName} API`, contextForReport, 'Turn.run-sendMessageStream');
            const status = typeof error === 'object' &&
                error !== null &&
                'status' in error &&
                typeof error.status === 'number'
                ? error.status
                : undefined;
            const structuredError = {
                message: getErrorMessage(error),
                status,
            };
            yield { type: GeminiEventType.Error, value: { error: structuredError } };
            return;
        }
    }
    handlePendingFunctionCall(fnCall) {
        const callId = fnCall.id ??
            `${fnCall.name}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
        // REAL FIX: Turn.ts also gets fragmented data - handle properly
        let name = fnCall.name;
        if (!name || name.trim() === '') {
            // Turn may get incomplete data from fragmented FunctionCalls
            // Keep undefined_tool_name for proper error detection
            name = 'undefined_tool_name';
        }
        else {
            // Apply shared normalization for defined names
            const normalized = normalizeToolName(name);
            if (normalized) {
                name = normalized;
            }
            else {
                name = 'undefined_tool_name';
            }
        }
        const args = (fnCall.args || {});
        const toolCallRequest = {
            callId,
            name: name || 'undefined_tool_name',
            args,
            isClientInitiated: false,
            prompt_id: this.prompt_id,
            agentId: this.agentId ?? DEFAULT_AGENT_ID,
        };
        this.pendingToolCalls.push(toolCallRequest);
        // Yield a request for the tool call, not the pending/confirming status
        return { type: GeminiEventType.ToolCallRequest, value: toolCallRequest };
    }
    getDebugResponses() {
        return this.debugResponses;
    }
}
//# sourceMappingURL=turn.js.map