/**
 * Conversation Memory Utilities
 * Handles configuration merging and conversation memory operations
 */
import { SpanKind, SpanStatusCode } from "@opentelemetry/api";
import { tracers } from "../telemetry/tracers.js";
import { withTimeout } from "./errorHandling.js";
import { DEFAULT_FALLBACK_THRESHOLD, getConversationMemoryDefaults, MEMORY_THRESHOLD_PERCENTAGE, } from "../config/conversationMemory.js";
import { getAvailableInputTokens } from "../constants/contextWindows.js";
import { buildSummarizationPrompt } from "../context/prompts/summarizationPrompt.js";
import { logger } from "./logger.js";
const memoryTracer = tracers.memory;
// Cached NeuroLink instance for summarization to avoid creating a new instance per call
let cachedSummarizer = null;
/**
 * Apply conversation memory defaults to user configuration
 * Merges user config with environment variables and default values
 */
export function applyConversationMemoryDefaults(userConfig) {
    const defaults = getConversationMemoryDefaults();
    return {
        ...defaults,
        ...userConfig,
    };
}
/**
 * Get conversation history as message array, summarizing if needed.
 */
export async function getConversationMessages(conversationMemory, options) {
    if (!conversationMemory || !options.context) {
        logger.warn("[conversationMemoryUtils] No memory or context, returning empty messages");
        return [];
    }
    const sessionId = options.context?.sessionId;
    if (typeof sessionId !== "string" || !sessionId) {
        logger.warn("[conversationMemoryUtils] Invalid or missing sessionId in context", {
            sessionIdType: typeof sessionId,
            sessionIdValue: sessionId,
        });
        return [];
    }
    return memoryTracer.startActiveSpan("neurolink.conversation.getMessages", {
        kind: SpanKind.INTERNAL,
        attributes: {
            "session.id": sessionId,
            "memory.type": conversationMemory.constructor.name,
        },
    }, async (span) => {
        try {
            // Extract userId from context
            const userId = options.context?.userId;
            if (userId) {
                span.setAttribute("user.id", userId);
            }
            const enableSummarization = options.enableSummarization ?? undefined;
            const messages = await conversationMemory.buildContextMessages(sessionId, userId, enableSummarization);
            span.setAttribute("message.count", messages.length);
            if (logger.shouldLog("debug")) {
                logger.debug("[conversationMemoryUtils] Conversation messages retrieved successfully", {
                    sessionId,
                    messageCount: messages.length,
                    messageTypes: messages.map((m) => m.role),
                });
            }
            return messages;
        }
        catch (error) {
            span.setStatus({
                code: SpanStatusCode.ERROR,
                message: error instanceof Error ? error.message : String(error),
            });
            span.recordException(error instanceof Error ? error : new Error(String(error)));
            logger.warn("[conversationMemoryUtils] Failed to get conversation messages", {
                sessionId,
                memoryType: conversationMemory.constructor.name,
                error: error instanceof Error ? error.message : String(error),
                stack: error instanceof Error ? error.stack : undefined,
            });
            return [];
        }
        finally {
            span.end();
        }
    });
}
/**
 * Store conversation turn for future context
 * Saves user messages and AI responses for conversation memory
 */
export async function storeConversationTurn(conversationMemory, originalOptions, result, startTimeStamp, requestId) {
    logger.debug("[conversationMemoryUtils] storeConversationTurn called", {
        requestId,
        hasMemory: !!conversationMemory,
        memoryType: conversationMemory?.constructor?.name || "NONE",
        hasContext: !!originalOptions.context,
        hasResult: !!result,
        resultContentLength: result?.content?.length || 0,
    });
    if (!conversationMemory || !originalOptions.context) {
        logger.debug("[conversationMemoryUtils] No memory or context, skipping conversation storage");
        return;
    }
    const context = originalOptions.context;
    const sessionId = context.sessionId;
    const userId = typeof context.userId === "string" ? context.userId : undefined;
    logger.debug("[conversationMemoryUtils] Extracted session details from context", {
        requestId,
        sessionId,
        userId,
        contextKeys: Object.keys(context),
        hasValidSessionId: typeof sessionId === "string" && !!sessionId,
    });
    if (typeof sessionId !== "string" || !sessionId) {
        logger.warn("[conversationMemoryUtils] Invalid or missing sessionId in context", {
            sessionIdType: typeof sessionId,
            sessionIdValue: sessionId,
        });
        return;
    }
    const userMessage = originalOptions.originalPrompt || originalOptions.prompt || "";
    const aiResponse = result.content ?? "";
    // Guard: skip storing conversation turn if AI response is empty AND no tools were used.
    // Empty assistant messages cause "text content blocks must be non-empty" errors
    // when loaded as conversation history on the next interaction.
    // However, tool-only turns (empty text but tools were invoked) must still be stored
    // to preserve tool-calling conversation history.
    const hasToolActivity = (result.toolsUsed && result.toolsUsed.length > 0) ||
        (result.toolExecutions && result.toolExecutions.length > 0);
    if (!aiResponse.trim() && !hasToolActivity) {
        logger.warn("[conversationMemoryUtils] Skipping conversation turn storage — AI response is empty and no tool activity", {
            sessionId,
            userId,
            userMessageLength: userMessage.length,
        });
        return;
    }
    let providerDetails;
    if (result.provider && result.model) {
        providerDetails = {
            provider: result.provider,
            model: result.model,
        };
    }
    await memoryTracer.startActiveSpan("neurolink.conversation.storeTurn", {
        kind: SpanKind.INTERNAL,
        attributes: {
            "session.id": sessionId,
            "content.length": userMessage.length + aiResponse.length,
        },
    }, async (span) => {
        if (userId) {
            span.setAttribute("user.id", userId);
        }
        try {
            await conversationMemory.storeConversationTurn({
                sessionId,
                userId,
                userMessage,
                aiResponse,
                startTimeStamp,
                providerDetails,
                enableSummarization: originalOptions.enableSummarization,
                requestId,
                tokenUsage: result.usage
                    ? {
                        inputTokens: result.usage.input,
                        outputTokens: result.usage.output,
                        totalTokens: result.usage.total,
                        cacheReadTokens: result.usage.cacheReadTokens,
                        cacheWriteTokens: result.usage.cacheCreationTokens,
                    }
                    : undefined,
                thoughtSignature: result.thoughtSignature,
            });
            logger.debug("[conversationMemoryUtils] Conversation turn stored successfully", {
                requestId,
                sessionId,
                userId,
                memoryType: conversationMemory.constructor.name,
                userMessageLength: userMessage.length,
                aiResponseLength: aiResponse.length,
            });
        }
        catch (error) {
            span.setStatus({
                code: SpanStatusCode.ERROR,
                message: error instanceof Error ? error.message : String(error),
            });
            span.recordException(error instanceof Error ? error : new Error(String(error)));
            const details = error?.details;
            logger.warn("[conversationMemoryUtils] Failed to store conversation turn", {
                sessionId,
                userId,
                memoryType: conversationMemory.constructor.name,
                error: error instanceof Error ? error.message : String(error),
                innerError: details?.error || "none",
                errorCode: error?.code || "unknown",
                stack: error instanceof Error ? error.stack : undefined,
            });
        }
        finally {
            span.end();
        }
    });
}
/**
 * Build context messages from pointer onwards (token-based memory)
 * Returns summary message (if exists) + all messages after the summarized pointer
 * @param session - Session memory with pointer
 * @returns Context messages to send to LLM
 */
export function buildContextFromPointer(session, requestId) {
    if (!session.summarizedUpToMessageId || !session.summarizedMessage) {
        // Log context built for LLM (no summary)
        const totalChars = session.messages.reduce((sum, msg) => sum + msg.content.length, 0);
        logger.info("[ConversationMemory] Context built for LLM", {
            requestId,
            sessionId: session.sessionId,
            contextMessages: session.messages.length,
            summaryPrepended: false,
            estimatedTokens: Math.ceil(totalChars / 4),
        });
        return session.messages;
    }
    // find a better way to wirte this
    const pointerIndex = session.messages.findIndex((msg) => msg.id === session.summarizedUpToMessageId);
    if (pointerIndex === -1) {
        logger.warn("Pointer message not found, returning all messages", {
            sessionId: session.sessionId,
            pointer: session.summarizedUpToMessageId,
            totalMessages: session.messages.length,
        });
        // Log context built for LLM (pointer not found fallback)
        const totalChars = session.messages.reduce((sum, msg) => sum + msg.content.length, 0);
        logger.info("[ConversationMemory] Context built for LLM", {
            requestId,
            sessionId: session.sessionId,
            contextMessages: session.messages.length,
            summaryPrepended: false,
            estimatedTokens: Math.ceil(totalChars / 4),
        });
        return session.messages;
    }
    const messagesAfterPointer = session.messages.slice(pointerIndex + 1);
    // Construct context: summary message + recent messages
    const summaryMessage = {
        id: `summary-${session.summarizedUpToMessageId}`,
        role: "user",
        content: `[Previous conversation summary]: ${session.summarizedMessage}`,
        timestamp: new Date().toISOString(),
        metadata: {
            isSummary: true,
            summarizesTo: session.summarizedUpToMessageId,
        },
    };
    logger.debug("Building context with summary", {
        sessionId: session.sessionId,
        pointerIndex,
        messagesAfterPointer: messagesAfterPointer.length,
        totalMessages: session.messages.length,
        summaryLength: session.summarizedMessage.length,
    });
    const contextMessages = [summaryMessage, ...messagesAfterPointer];
    // Log context built for LLM with structural metadata
    const totalChars = contextMessages.reduce((sum, msg) => sum + msg.content.length, 0);
    logger.info("[ConversationMemory] Context built for LLM", {
        requestId,
        sessionId: session.sessionId,
        contextMessages: contextMessages.length,
        summaryPrepended: true,
        estimatedTokens: Math.ceil(totalChars / 4),
    });
    return contextMessages;
}
/**
 * Create summarization prompt from message history
 * Used by both in-memory and Redis conversation managers
 * @param history - Messages to summarize
 * @param previousSummary - Optional previous summary to build upon
 */
export function createSummarizationPrompt(history, previousSummary) {
    const formattedHistory = history
        .map((msg) => `${msg.role}: ${msg.content}`)
        .join("\n\n");
    const structuredPrompt = buildSummarizationPrompt({
        isIncremental: !!previousSummary,
        previousSummary,
    });
    return `${structuredPrompt}

Conversation History to Summarize:
---
${formattedHistory}
---`;
}
/**
 * Calculate token threshold based on model's context window and available input tokens
 * Uses context window registry for accurate per-provider, per-model limits
 * @param provider - AI provider name
 * @param model - Model name
 * @param maxTokens - Optional explicit maxTokens for output reserve calculation
 * @returns Token threshold (80% of available input tokens)
 */
export function calculateTokenThreshold(provider, model, maxTokens) {
    if (!provider) {
        return DEFAULT_FALLBACK_THRESHOLD;
    }
    const availableInput = getAvailableInputTokens(provider, model, maxTokens);
    if (availableInput <= 0) {
        return DEFAULT_FALLBACK_THRESHOLD;
    }
    return Math.floor(availableInput * MEMORY_THRESHOLD_PERCENTAGE);
}
/**
 * Get effective token threshold for a session
 * Priority: session override > env var > model-based (80%) > fallback
 * @param provider - AI provider name
 * @param model - Model name
 * @param envOverride - Environment variable override
 * @param sessionOverride - Per-session token threshold override
 * @returns Effective token threshold
 */
export function getEffectiveTokenThreshold(provider, model, envOverride, sessionOverride) {
    // Priority 1: Session-level override
    if (sessionOverride && sessionOverride > 0) {
        return sessionOverride;
    }
    // Priority 2: Environment variable override
    if (envOverride && envOverride > 0) {
        return envOverride;
    }
    // Priority 3: Model-based calculation (80% of context window)
    try {
        return calculateTokenThreshold(provider, model);
    }
    catch (error) {
        logger.warn("Failed to calculate effective threshold, using fallback", {
            provider,
            model,
            error: error instanceof Error ? error.message : String(error),
        });
        // Priority 4: Fallback for unknown models
        return DEFAULT_FALLBACK_THRESHOLD;
    }
}
/**
 * Generate summary using configured provider and model
 * Centralized summarization logic used by both ConversationMemoryManager and RedisConversationMemoryManager
 * @param messages - Messages to summarize
 * @param config - Conversation memory configuration containing provider/model settings
 * @param previousSummary - Optional previous summary to build upon
 * @param logPrefix - Prefix for log messages (e.g., "[ConversationMemory]" or "[RedisConversationMemoryManager]")
 * @param requestId - Optional request ID for request-scoped tracing
 * @returns Summary text or null if generation fails
 */
export async function generateSummary(messages, config, logPrefix = "[ConversationMemory]", previousSummary, requestId) {
    const summarizationPrompt = createSummarizationPrompt(messages, previousSummary);
    const SUMMARIZER_INIT_TIMEOUT = 15_000;
    const SUMMARIZER_GENERATE_TIMEOUT = 60_000;
    try {
        if (!cachedSummarizer) {
            cachedSummarizer = await withTimeout((async () => {
                const { NeuroLink: NeuroLinkClass } = await import("../neurolink.js");
                return new NeuroLinkClass({
                    conversationMemory: { enabled: false },
                });
            })(), SUMMARIZER_INIT_TIMEOUT, new Error("Summarizer initialization timed out"));
        }
        if (!config.summarizationProvider || !config.summarizationModel) {
            logger.error(`${logPrefix} Missing summarization provider`, {
                requestId,
            });
            return null;
        }
        const summaryResult = await withTimeout(cachedSummarizer.generate({
            input: { text: summarizationPrompt },
            provider: config.summarizationProvider,
            model: config.summarizationModel,
            disableTools: true,
        }), SUMMARIZER_GENERATE_TIMEOUT, new Error("Summary generation timed out"));
        return summaryResult.content || null;
    }
    catch (error) {
        logger.error(`${logPrefix} Error generating summary`, { requestId, error });
        return null;
    }
}
/**
 * Check if Redis is available for conversation memory.
 * Migrated from the deprecated conversationMemoryUtils.ts.
 */
export async function checkRedisAvailability() {
    const { createRedisClient, getNormalizedConfig } = await import("./redis.js");
    let testClient = null;
    try {
        const testConfig = getNormalizedConfig({
            host: process.env.REDIS_HOST,
            port: process.env.REDIS_PORT ? Number(process.env.REDIS_PORT) : undefined,
            password: process.env.REDIS_PASSWORD,
            db: process.env.REDIS_DB ? Number(process.env.REDIS_DB) : undefined,
            keyPrefix: process.env.REDIS_KEY_PREFIX,
            ttl: process.env.REDIS_TTL ? Number(process.env.REDIS_TTL) : undefined,
            connectionOptions: {
                connectTimeout: 5000,
                maxRetriesPerRequest: 1,
                retryDelayOnFailover: 100,
            },
        });
        testClient = await createRedisClient(testConfig);
        await testClient.ping();
        logger.debug("Redis connection test successful");
        return true;
    }
    catch (error) {
        logger.debug("Redis connection test failed", {
            error: error instanceof Error ? error.message : String(error),
        });
        return false;
    }
    finally {
        if (testClient) {
            try {
                await testClient.quit();
            }
            catch (quitError) {
                logger.debug("Error during Redis test client disconnect", {
                    error: quitError instanceof Error ? quitError.message : String(quitError),
                });
            }
        }
    }
}
