import { FileLoader } from "../utils/FileLoader.js";
import { isBinaryContent, formatMultimodalContent } from "./Content.js";
import { Executor } from "../executor/Executor.js";
import { ChatStream } from "./ChatStream.js";
import { ModelRegistry } from "../models/ModelRegistry.js";
import { Schema } from "../schema/Schema.js";
import { toJsonSchema } from "../schema/to-json-schema.js";
import { randomUUID } from "node:crypto";
import { z } from "zod";
import { config } from "../config.js";
import { ToolExecutionMode } from "../constants.js";
import { ConfigurationError } from "../errors/index.js";
import { ChatValidator } from "./Validation.js";
import { ToolHandler } from "./ToolHandler.js";
import { logger } from "../utils/logger.js";
import { runMiddleware } from "../utils/middleware-runner.js";
import { ChatResponseString } from "./ChatResponse.js";
export class Chat {
    provider;
    model;
    options;
    messages = [];
    systemMessages = [];
    executor;
    middlewares = [];
    constructor(provider, model, options = {}, retryConfig = { attempts: 1, delayMs: 0 }) {
        this.provider = provider;
        this.model = model;
        this.options = options;
        this.middlewares = options.middlewares || [];
        this.executor = new Executor(provider, retryConfig);
        if (options.systemPrompt) {
            this.withInstructions(options.systemPrompt);
        }
        if (options.messages) {
            for (const msg of options.messages) {
                if (msg.role === "system" || msg.role === "developer") {
                    this.systemMessages.push(msg);
                }
                else {
                    this.messages.push(msg);
                }
            }
        }
        if (!this.options.toolExecution) {
            this.options.toolExecution = config.toolExecution || ToolExecutionMode.AUTO;
        }
        if (options.tools) {
            const toolList = options.tools;
            this.options.tools = []; // Clear and re-add via normalized method
            this.withTools(toolList);
        }
    }
    /**
     * Read-only access to message history
     */
    get history() {
        return [...this.systemMessages, ...this.messages];
    }
    get modelId() {
        return this.model;
    }
    /**
     * Aggregate usage across the entire conversation
     */
    get totalUsage() {
        return this.messages.reduce((acc, msg) => {
            if (msg.usage) {
                acc.input_tokens += msg.usage.input_tokens;
                acc.output_tokens += msg.usage.output_tokens;
                acc.total_tokens += msg.usage.total_tokens;
                acc.cached_tokens = (acc.cached_tokens ?? 0) + (msg.usage.cached_tokens ?? 0);
                acc.cache_creation_tokens =
                    (acc.cache_creation_tokens ?? 0) + (msg.usage.cache_creation_tokens ?? 0);
                acc.cost = (acc.cost ?? 0) + (msg.usage.cost ?? 0);
            }
            return acc;
        }, {
            input_tokens: 0,
            output_tokens: 0,
            total_tokens: 0,
            cached_tokens: 0,
            cache_creation_tokens: 0,
            cost: 0
        });
    }
    /**
     * Add a tool to the chat session (fluent API)
     * Supports passing a tool instance or a tool class (which will be instantiated).
     */
    withTool(tool) {
        return this.withTools([tool]);
    }
    /**
     * Add multiple tools to the chat session.
     * Supports passing Tool classes (which will be instantiated) or instances.
     * Can replace existing tools if options.replace is true.
     *
     * @example
     * chat.withTools([WeatherTool, new CalculatorTool()], { replace: true });
     */
    withTools(tools, options) {
        if (options?.replace) {
            this.options.tools = [];
        }
        if (!this.options.tools) {
            this.options.tools = [];
        }
        for (const tool of tools) {
            const normalized = this.normalizeTool(tool);
            if (normalized) {
                this.options.tools.push(normalized);
            }
        }
        return this;
    }
    /**
     * Add instructions (system prompt) to the chat.
     * By default, it appends a new system message.
     * If { replace: true } is passed, it removes all previous system messages first.
     */
    withInstructions(instruction, options) {
        if (options?.replace) {
            this.systemMessages = [];
        }
        // Always push to isolated storage
        this.systemMessages.push({ role: "system", content: instruction });
        return this;
    }
    /**
     * Alias for withInstructions
     */
    withSystemPrompt(instruction, options) {
        return this.withInstructions(instruction, options);
    }
    /**
     * Alias for withInstructions
     */
    system(instruction, options) {
        return this.withInstructions(instruction, options);
    }
    /**
     * Add a message manually to the chat history.
     * Useful for rehydrating sessions from a database.
     */
    add(role, content) {
        // Ensure content matches MessageContent type
        const safeContent = content;
        if (role === "system" || role === "developer") {
            this.systemMessages.push({ role, content: safeContent });
        }
        else {
            this.messages.push({ role, content: safeContent });
        }
        return this;
    }
    /**
     * Add a raw Message object to the chat history.
     */
    addMessage(message) {
        if (message.role === "system" || message.role === "developer") {
            this.systemMessages.push(message);
        }
        else {
            this.messages.push(message);
        }
        return this;
    }
    /**
     * Set the temperature for the chat session.
     * Controls randomness: 0.0 (deterministic) to 1.0 (creative).
     */
    withTemperature(temp) {
        this.options.temperature = temp;
        return this;
    }
    /**
     * Switch the model used for this chat session.
     */
    withModel(model) {
        this.model = model;
        return this;
    }
    /**
     * Set custom headers for the chat session.
     * Merges with existing headers.
     */
    withRequestOptions(options) {
        if (options.headers) {
            this.options.headers = { ...this.options.headers, ...options.headers };
        }
        if (options.responseFormat) {
            this.options.responseFormat = options.responseFormat;
        }
        return this;
    }
    /**
     * Set provider-specific parameters.
     * These will be merged into the final request payload.
     */
    withParams(params) {
        this.options.params = { ...this.options.params, ...params };
        return this;
    }
    /**
     * Enforce a specific schema for the output.
     * Can accept a Schema object or a Zod schema/JSON Schema directly.
     */
    withSchema(schema) {
        if (schema === null) {
            this.options.schema = undefined;
            return this;
        }
        if (schema instanceof Schema) {
            this.options.schema = schema;
        }
        else if (schema instanceof z.ZodType) {
            this.options.schema = Schema.fromZod("output", schema);
        }
        else {
            this.options.schema = Schema.fromJson("output", schema);
        }
        return this;
    }
    /**
     * Provide a prediction of the expected output to reduce latency.
     * Particularly useful for code-editing agents modifying existing text.
     */
    withPrediction(prediction) {
        if (prediction === null) {
            delete this.options.prediction;
        }
        else {
            this.options.prediction = prediction;
        }
        return this;
    }
    /**
     * Enable and configure extended thinking for reasoning models.
     */
    withThinking(config) {
        this.options.thinking = { ...this.options.thinking, ...config };
        return this;
    }
    /**
     * Shortcut to set thinking effort.
     */
    withEffort(effort) {
        return this.withThinking({ effort });
    }
    // --- Event Handlers ---
    onNewMessage(handler) {
        this.options.onNewMessage = handler;
        return this;
    }
    onEndMessage(handler) {
        this.options.onEndMessage = handler;
        return this;
    }
    onToolCall(handler) {
        return this.onToolCallStart(handler);
    }
    onToolResult(handler) {
        return this.onToolCallEnd((_call, result) => handler(result));
    }
    /**
     * Called when a tool call starts.
     */
    onToolCallStart(handler) {
        this.options.onToolCallStart = handler;
        return this;
    }
    /**
     * Called when a tool call ends successfully.
     */
    onToolCallEnd(handler) {
        this.options.onToolCallEnd = handler;
        return this;
    }
    onToolCallError(handler) {
        this.options.onToolCallError = handler;
        return this;
    }
    /**
     * Set the tool execution mode.
     * - "auto": (Default) Automatically execute all tool calls.
     * - "confirm": Call onConfirmToolCall before executing each tool.
     * - "dry-run": Propose tool calls but do not execute them.
     */
    withToolExecution(mode) {
        this.options.toolExecution = mode;
        return this;
    }
    /**
     * Hook for confirming tool execution in "confirm" mode.
     * Return true to proceed, false to cancel the specific call.
     */
    onConfirmToolCall(handler) {
        this.options.onConfirmToolCall = handler;
        return this;
    }
    /**
     * Add a hook to process messages before sending to the LLM.
     * Useful for PII detection, redaction, and input moderation.
     */
    beforeRequest(handler) {
        this.options.onBeforeRequest = handler;
        return this;
    }
    /**
     * Add a hook to process the response before returning it.
     * Useful for output redaction, compliance, and post-moderation.
     */
    afterResponse(handler) {
        this.options.onAfterResponse = handler;
        return this;
    }
    /**
     * Ask the model a question
     */
    async ask(content, options) {
        const requestId = randomUUID();
        const state = {};
        let messageContent = content;
        const files = [...(options?.images ?? []), ...(options?.files ?? [])];
        if (files.length > 0) {
            const processedFiles = await Promise.all(files.map((f) => FileLoader.load(f)));
            const hasBinary = processedFiles.some(isBinaryContent);
            ChatValidator.validateVision(this.provider, this.model, hasBinary, this.options);
            messageContent = formatMultimodalContent(content, processedFiles);
        }
        if (this.options.tools && this.options.tools.length > 0) {
            ChatValidator.validateTools(this.provider, this.model, true, this.options);
        }
        this.messages.push({
            role: "user",
            content: messageContent
        });
        // Process Schema/Structured Output
        let responseFormat = this.options.responseFormat;
        if (this.options.schema) {
            ChatValidator.validateStructuredOutput(this.provider, this.model, true, this.options);
            const jsonSchema = toJsonSchema(this.options.schema.definition.schema);
            responseFormat = {
                type: "json_schema",
                json_schema: {
                    name: this.options.schema.definition.name,
                    description: this.options.schema.definition.description,
                    strict: this.options.schema.definition.strict ?? true,
                    schema: jsonSchema
                }
            };
        }
        // Prepare Middleware Context
        const context = {
            requestId,
            provider: this.provider.id,
            model: this.model,
            messages: [...this.systemMessages, ...this.messages],
            options: this.options,
            state
        };
        try {
            // 1. onRequest Hook
            await runMiddleware(this.middlewares, "onRequest", context);
            let assistantMessage;
            let finalResponse;
            let correctionAttempt = 0;
            const totalUsage = { input_tokens: 0, output_tokens: 0, total_tokens: 0 };
            // Self-Correction Loop
            while (true) {
                // Prepare execution options
                const messagesToUse = context.messages || [];
                const executeOptions = {
                    model: this.model,
                    messages: messagesToUse,
                    tools: this.options.tools,
                    temperature: options?.temperature ?? this.options.temperature,
                    max_tokens: options?.maxTokens ?? this.options.maxTokens ?? config.maxTokens,
                    headers: { ...this.options.headers, ...options?.headers },
                    response_format: responseFormat, // Pass to provider
                    requestTimeout: options?.requestTimeout ?? this.options.requestTimeout ?? config.requestTimeout,
                    thinking: options?.thinking ?? this.options.thinking,
                    prediction: options?.prediction ?? this.options.prediction,
                    signal: options?.signal,
                    ...this.options.params
                };
                // --- Content Policy Hooks (Input) ---
                if (this.options.onBeforeRequest) {
                    const result = await this.options.onBeforeRequest(executeOptions.messages);
                    if (result) {
                        executeOptions.messages = result;
                    }
                }
                const trackUsage = (u) => {
                    if (u) {
                        if (u.cost === undefined) {
                            const withCost = ModelRegistry.calculateCost(u, this.model, this.provider.id);
                            u.cost = withCost.cost;
                            u.input_cost = withCost.input_cost;
                            u.output_cost = withCost.output_cost;
                        }
                        totalUsage.input_tokens += u.input_tokens;
                        totalUsage.output_tokens += u.output_tokens;
                        totalUsage.total_tokens += u.total_tokens;
                        if (u.cached_tokens) {
                            totalUsage.cached_tokens = (totalUsage.cached_tokens ?? 0) + u.cached_tokens;
                        }
                        if (u.cost !== undefined) {
                            totalUsage.cost = (totalUsage.cost ?? 0) + u.cost;
                        }
                        if (u.input_cost !== undefined) {
                            totalUsage.input_cost = (totalUsage.input_cost ?? 0) + u.input_cost;
                        }
                        if (u.output_cost !== undefined) {
                            totalUsage.output_cost = (totalUsage.output_cost ?? 0) + u.output_cost;
                        }
                    }
                };
                // Start generation
                if (this.options.onNewMessage)
                    this.options.onNewMessage();
                let response = await this.executor.executeChat(executeOptions);
                trackUsage(response.usage);
                assistantMessage = new ChatResponseString(response.content ?? "", response.usage ?? { input_tokens: 0, output_tokens: 0, total_tokens: 0 }, this.model, this.provider.id, response.thinking, response.reasoning, response.tool_calls, response.finish_reason, this.options.schema);
                // --- Content Policy Hooks (Output - Turn 1) ---
                if (this.options.onAfterResponse) {
                    const result = await this.options.onAfterResponse(assistantMessage);
                    if (result) {
                        assistantMessage = result;
                    }
                }
                // Add to history (temporary, might be rolled back on retry)
                this.messages.push({
                    role: "assistant",
                    content: assistantMessage?.toString() || null,
                    tool_calls: response.tool_calls,
                    usage: response.usage
                });
                if (this.options.onEndMessage &&
                    (!response.tool_calls || response.tool_calls.length === 0)) {
                    this.options.onEndMessage(assistantMessage);
                }
                const maxToolCalls = options?.maxToolCalls ?? this.options.maxToolCalls ?? 5;
                let stepCount = 0;
                let haltTriggered = false;
                // Tool Calling Loop
                while (response.tool_calls && response.tool_calls.length > 0 && !haltTriggered) {
                    if (!ToolHandler.shouldExecuteTools(response.tool_calls, this.options.toolExecution)) {
                        break;
                    }
                    stepCount++;
                    if (stepCount > maxToolCalls) {
                        throw new Error(`[NodeLLM] Maximum tool execution calls (${maxToolCalls}) exceeded.`);
                    }
                    for (const toolCall of response.tool_calls) {
                        if (this.options.toolExecution === ToolExecutionMode.CONFIRM) {
                            const approved = await ToolHandler.requestToolConfirmation(toolCall, this.options.onConfirmToolCall);
                            if (!approved) {
                                this.messages.push(this.provider.formatToolResultMessage(toolCall.id, "Action cancelled by user."));
                                continue;
                            }
                        }
                        await runMiddleware(this.middlewares, "onToolCallStart", context, toolCall);
                        try {
                            const toolResult = await ToolHandler.execute(toolCall, this.options.tools, this.options.onToolCallStart, this.options.onToolCallEnd);
                            await runMiddleware(this.middlewares, "onToolCallEnd", context, toolCall, toolResult.content);
                            this.messages.push(this.provider.formatToolResultMessage(toolResult.tool_call_id, toolResult.content));
                            if (toolResult.halted) {
                                haltTriggered = true;
                                assistantMessage = new ChatResponseString(toolResult.content, totalUsage, this.model, this.provider.id, undefined, undefined, undefined, "tool_halt");
                                if (this.options.onEndMessage) {
                                    this.options.onEndMessage(assistantMessage);
                                }
                                break;
                            }
                        }
                        catch (error) {
                            let currentError = error;
                            const middlewareDirective = await runMiddleware(this.middlewares, "onToolCallError", context, toolCall, currentError);
                            const directive = middlewareDirective ||
                                (await this.options.onToolCallError?.(toolCall, currentError));
                            if (directive === "STOP")
                                throw currentError;
                            if (directive === "RETRY") {
                                try {
                                    const toolResult = await ToolHandler.execute(toolCall, this.options.tools, this.options.onToolCallStart, this.options.onToolCallEnd);
                                    await runMiddleware(this.middlewares, "onToolCallEnd", context, toolCall, toolResult.content);
                                    this.messages.push(this.provider.formatToolResultMessage(toolResult.tool_call_id, toolResult.content));
                                    continue;
                                }
                                catch (retryError) {
                                    currentError = retryError;
                                    await runMiddleware(this.middlewares, "onToolCallError", context, toolCall, currentError);
                                }
                            }
                            this.messages.push(this.provider.formatToolResultMessage(toolCall.id, `Fatal error executing tool '${toolCall.function.name}': ${currentError.message}`, { isError: true }));
                            if (directive === "CONTINUE")
                                continue;
                            const errorObj = currentError;
                            if (errorObj.fatal || errorObj.status === 401 || errorObj.status === 403)
                                throw currentError;
                            logger.error(`Tool execution failed:`, currentError);
                        }
                    }
                    if (haltTriggered)
                        break;
                    response = await this.executor.executeChat({
                        model: this.model,
                        messages: [...this.systemMessages, ...this.messages],
                        tools: this.options.tools,
                        temperature: options?.temperature ?? this.options.temperature,
                        max_tokens: options?.maxTokens ?? this.options.maxTokens ?? config.maxTokens,
                        headers: this.options.headers,
                        response_format: responseFormat,
                        requestTimeout: options?.requestTimeout ?? this.options.requestTimeout ?? config.requestTimeout,
                        prediction: options?.prediction ?? this.options.prediction,
                        signal: options?.signal,
                        ...this.options.params
                    });
                    trackUsage(response.usage);
                    assistantMessage = new ChatResponseString(response.content ?? "", response.usage ?? { input_tokens: 0, output_tokens: 0, total_tokens: 0 }, this.model, this.provider.id, response.thinking, response.reasoning, response.tool_calls, response.finish_reason, this.options.schema);
                    if (this.options.onAfterResponse) {
                        const result = await this.options.onAfterResponse(assistantMessage);
                        if (result)
                            assistantMessage = result;
                    }
                    this.messages.push({
                        role: "assistant",
                        content: assistantMessage?.toString() || null,
                        tool_calls: response.tool_calls,
                        usage: response.usage
                    });
                    if (this.options.onEndMessage &&
                        (!response.tool_calls || response.tool_calls.length === 0)) {
                        this.options.onEndMessage(assistantMessage);
                    }
                }
                finalResponse = new ChatResponseString(assistantMessage.toString() || "", totalUsage, this.model, this.provider.id, assistantMessage.thinking, assistantMessage.reasoning, response.tool_calls, assistantMessage.finish_reason, this.options.schema);
                // 5. onResponse Hook
                const requestDirective = await runMiddleware(this.middlewares, "onResponse", context, finalResponse);
                if (requestDirective && typeof requestDirective === "object") {
                    if (requestDirective.action === "RETRY") {
                        // Push feedback to history and try again
                        this.messages.push({
                            role: "user",
                            content: requestDirective.message
                        });
                        correctionAttempt++;
                        continue;
                    }
                    if (requestDirective.action === "REPLACE") {
                        return requestDirective.response;
                    }
                }
                if (requestDirective === "STOP") {
                    throw new Error("[NodeLLM] Execution stopped by middleware.");
                }
                return finalResponse;
            }
        }
        catch (err) {
            // 6. onError Hook
            const errorDirective = await runMiddleware(this.middlewares, "onError", context, err);
            if (errorDirective &&
                typeof errorDirective === "object" &&
                errorDirective.action === "RETRY") {
                // ... handled by retry loop?
                // For now, only onResponse triggers self-correction for simplicity.
            }
            throw err;
        }
    }
    /**
     * Streams the model's response to a user question.
     */
    stream(content, options = {}) {
        const streamer = new ChatStream(this.provider, this.model, this.options, this.messages, this.systemMessages);
        return streamer.create(content, options);
    }
    /**
     * Normalizes a ToolResolvable into a ToolDefinition.
     */
    normalizeTool(tool) {
        let toolInstance;
        // Handle class constructor
        if (typeof tool === "function") {
            try {
                toolInstance = new tool();
            }
            catch (e) {
                logger.error(`Failed to instantiate tool class: ${tool.name}`, e);
                return null;
            }
        }
        else {
            toolInstance = tool;
        }
        if (!toolInstance)
            return null;
        // Normalized to standard ToolDefinition interface if it's a Tool class instance
        if (typeof toolInstance.toLLMTool === "function") {
            return toolInstance.toLLMTool();
        }
        const toolObj = toolInstance;
        if (!toolObj.function || !toolObj.function.name) {
            // 1. Validate structure
            throw new ConfigurationError(`[NodeLLM] Tool validation failed: 'function.name' is required for raw tool objects.`);
        }
        if (toolObj.type !== "function") {
            // 2. Ensure 'type: function' exists (standardize for providers)
            toolObj.type = "function";
        }
        if (typeof toolObj.handler !== "function") {
            // 3. Validate handler existence
            throw new ConfigurationError(`[NodeLLM] Tool validation failed: Tool '${toolObj.function.name}' must have a 'handler' function. (Note: Only Tool subclasses use 'execute()')`);
        }
        return toolInstance;
    }
}
