/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { partListUnionToString, SESSION_FILE_PREFIX, } from '@google/gemini-cli-core';
import * as fs from 'node:fs/promises';
import path from 'node:path';
import { stripUnsafeCharacters } from '../ui/utils/textUtils.js';
import { checkExhaustive } from './checks.js';
import { MessageType, ToolCallStatus, } from '../ui/types.js';
/**
 * Constant for the resume "latest" identifier.
 * Used when --resume is passed without a value to select the most recent session.
 */
export const RESUME_LATEST = 'latest';
/**
 * Error thrown for session-related failures.
 * Uses a code field to differentiate between error types.
 */
export class SessionError extends Error {
    code;
    constructor(code, message) {
        super(message);
        this.code = code;
        this.name = 'SessionError';
    }
    /**
     * Creates an error for when no sessions exist for the current project.
     */
    static noSessionsFound() {
        return new SessionError('NO_SESSIONS_FOUND', 'No previous sessions found for this project.');
    }
    /**
     * Creates an error for when a session identifier is invalid.
     */
    static invalidSessionIdentifier(identifier) {
        return new SessionError('INVALID_SESSION_IDENTIFIER', `Invalid session identifier "${identifier}".\n  Use --list-sessions to see available sessions, then use --resume {number}, --resume {uuid}, or --resume latest.`);
    }
}
/**
 * Checks if a session has at least one user or assistant (gemini) message.
 * Sessions with only system messages (info, error, warning) are considered empty.
 * @param messages - The array of message records to check
 * @returns true if the session has meaningful content
 */
export const hasUserOrAssistantMessage = (messages) => messages.some((msg) => msg.type === 'user' || msg.type === 'gemini');
/**
 * Cleans and sanitizes message content for display by:
 * - Converting newlines to spaces
 * - Collapsing multiple whitespace to single spaces
 * - Removing non-printable characters (keeping only ASCII 32-126)
 * - Trimming leading/trailing whitespace
 * @param message - The raw message content to clean
 * @returns Sanitized message suitable for display
 */
export const cleanMessage = (message) => message
    .replace(/\n+/g, ' ')
    .replace(/\s+/g, ' ')
    .replace(/[^\x20-\x7E]+/g, '') // Non-printable.
    .trim();
/**
 * Extracts the first meaningful user message from conversation messages.
 */
export const extractFirstUserMessage = (messages) => {
    const userMessage = messages
        // First try filtering out slash commands.
        .filter((msg) => {
        const content = partListUnionToString(msg.content);
        return (!content.startsWith('/') &&
            !content.startsWith('?') &&
            content.trim().length > 0);
    })
        .find((msg) => msg.type === 'user');
    let content;
    if (!userMessage) {
        // Fallback to first user message even if it's a slash command
        const firstMsg = messages.find((msg) => msg.type === 'user');
        if (!firstMsg)
            return 'Empty conversation';
        content = cleanMessage(partListUnionToString(firstMsg.content));
    }
    else {
        content = cleanMessage(partListUnionToString(userMessage.content));
    }
    return content;
};
/**
 * Formats a timestamp as relative time.
 * @param timestamp - The timestamp to format
 * @param style - 'long' (e.g. "2 hours ago") or 'short' (e.g. "2h")
 */
export const formatRelativeTime = (timestamp, style = 'long') => {
    const now = new Date();
    const time = new Date(timestamp);
    const diffMs = now.getTime() - time.getTime();
    const diffSeconds = Math.floor(diffMs / 1000);
    const diffMinutes = Math.floor(diffSeconds / 60);
    const diffHours = Math.floor(diffMinutes / 60);
    const diffDays = Math.floor(diffHours / 24);
    if (style === 'short') {
        if (diffSeconds < 1)
            return 'now';
        if (diffSeconds < 60)
            return `${diffSeconds}s`;
        if (diffMinutes < 60)
            return `${diffMinutes}m`;
        if (diffHours < 24)
            return `${diffHours}h`;
        if (diffDays < 30)
            return `${diffDays}d`;
        const diffMonths = Math.floor(diffDays / 30);
        return diffMonths < 12
            ? `${diffMonths}mo`
            : `${Math.floor(diffMonths / 12)}y`;
    }
    else {
        if (diffDays > 0) {
            return `${diffDays} day${diffDays === 1 ? '' : 's'} ago`;
        }
        else if (diffHours > 0) {
            return `${diffHours} hour${diffHours === 1 ? '' : 's'} ago`;
        }
        else if (diffMinutes > 0) {
            return `${diffMinutes} minute${diffMinutes === 1 ? '' : 's'} ago`;
        }
        else {
            return 'Just now';
        }
    }
};
/**
 * Loads all session files (including corrupted ones) from the chats directory.
 * @returns Array of session file entries, with sessionInfo null for corrupted files
 */
export const getAllSessionFiles = async (chatsDir, currentSessionId, options = {}) => {
    try {
        const files = await fs.readdir(chatsDir);
        const sessionFiles = files
            .filter((f) => f.startsWith(SESSION_FILE_PREFIX) && f.endsWith('.json'))
            .sort(); // Sort by filename, which includes timestamp
        const sessionPromises = sessionFiles.map(async (file) => {
            const filePath = path.join(chatsDir, file);
            try {
                const content = JSON.parse(await fs.readFile(filePath, 'utf8'));
                // Validate required fields
                if (!content.sessionId ||
                    !content.messages ||
                    !Array.isArray(content.messages) ||
                    !content.startTime ||
                    !content.lastUpdated) {
                    // Missing required fields - treat as corrupted
                    return { fileName: file, sessionInfo: null };
                }
                // Skip sessions that only contain system messages (info, error, warning)
                if (!hasUserOrAssistantMessage(content.messages)) {
                    return { fileName: file, sessionInfo: null };
                }
                const firstUserMessage = extractFirstUserMessage(content.messages);
                const isCurrentSession = currentSessionId
                    ? file.includes(currentSessionId.slice(0, 8))
                    : false;
                let fullContent;
                let messages;
                if (options.includeFullContent) {
                    fullContent = content.messages
                        .map((msg) => partListUnionToString(msg.content))
                        .join(' ');
                    messages = content.messages.map((msg) => ({
                        role: msg.type === 'user'
                            ? 'user'
                            : 'assistant',
                        content: partListUnionToString(msg.content),
                    }));
                }
                const sessionInfo = {
                    id: content.sessionId,
                    file: file.replace('.json', ''),
                    fileName: file,
                    startTime: content.startTime,
                    lastUpdated: content.lastUpdated,
                    messageCount: content.messages.length,
                    displayName: content.summary
                        ? stripUnsafeCharacters(content.summary)
                        : firstUserMessage,
                    firstUserMessage,
                    isCurrentSession,
                    index: 0, // Will be set after sorting valid sessions
                    summary: content.summary,
                    fullContent,
                    messages,
                };
                return { fileName: file, sessionInfo };
            }
            catch {
                // File is corrupted (can't read or parse JSON)
                return { fileName: file, sessionInfo: null };
            }
        });
        return await Promise.all(sessionPromises);
    }
    catch (error) {
        // It's expected that the directory might not exist, which is not an error.
        if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
            return [];
        }
        // For other errors (e.g., permissions), re-throw to be handled by the caller.
        throw error;
    }
};
/**
 * Loads all valid session files from the chats directory and converts them to SessionInfo.
 * Corrupted files are automatically filtered out.
 */
export const getSessionFiles = async (chatsDir, currentSessionId, options = {}) => {
    const allFiles = await getAllSessionFiles(chatsDir, currentSessionId, options);
    // Filter out corrupted files and extract SessionInfo
    const validSessions = allFiles
        .filter((entry) => entry.sessionInfo !== null)
        .map((entry) => entry.sessionInfo);
    // Deduplicate sessions by ID
    const uniqueSessionsMap = new Map();
    for (const session of validSessions) {
        // If duplicate exists, keep the one with the later lastUpdated timestamp
        if (!uniqueSessionsMap.has(session.id) ||
            new Date(session.lastUpdated).getTime() >
                new Date(uniqueSessionsMap.get(session.id).lastUpdated).getTime()) {
            uniqueSessionsMap.set(session.id, session);
        }
    }
    const uniqueSessions = Array.from(uniqueSessionsMap.values());
    // Sort by startTime (oldest first) for stable session numbering
    uniqueSessions.sort((a, b) => new Date(a.startTime).getTime() - new Date(b.startTime).getTime());
    // Set the correct 1-based indexes after sorting
    uniqueSessions.forEach((session, index) => {
        session.index = index + 1;
    });
    return uniqueSessions;
};
/**
 * Utility class for session discovery and selection.
 */
export class SessionSelector {
    config;
    constructor(config) {
        this.config = config;
    }
    /**
     * Lists all available sessions for the current project.
     */
    async listSessions() {
        const chatsDir = path.join(this.config.storage.getProjectTempDir(), 'chats');
        return getSessionFiles(chatsDir, this.config.getSessionId());
    }
    /**
     * Finds a session by identifier (UUID or numeric index).
     *
     * @param identifier - Can be a full UUID or an index number (1-based)
     * @returns Promise resolving to the found SessionInfo
     * @throws Error if the session is not found or identifier is invalid
     */
    async findSession(identifier) {
        const sessions = await this.listSessions();
        if (sessions.length === 0) {
            throw SessionError.noSessionsFound();
        }
        // Sort by startTime (oldest first, so newest sessions get highest numbers)
        const sortedSessions = sessions.sort((a, b) => new Date(a.startTime).getTime() - new Date(b.startTime).getTime());
        // Try to find by UUID first
        const sessionByUuid = sortedSessions.find((session) => session.id === identifier);
        if (sessionByUuid) {
            return sessionByUuid;
        }
        // Parse as index number (1-based) - only allow numeric indexes
        const index = parseInt(identifier, 10);
        if (!isNaN(index) &&
            index.toString() === identifier &&
            index > 0 &&
            index <= sortedSessions.length) {
            return sortedSessions[index - 1];
        }
        throw SessionError.invalidSessionIdentifier(identifier);
    }
    /**
     * Resolves a resume argument to a specific session.
     *
     * @param resumeArg - Can be "latest", a full UUID, or an index number (1-based)
     * @returns Promise resolving to session selection result
     */
    async resolveSession(resumeArg) {
        let selectedSession;
        if (resumeArg === RESUME_LATEST) {
            const sessions = await this.listSessions();
            if (sessions.length === 0) {
                throw new Error('No previous sessions found for this project.');
            }
            // Sort by startTime (oldest first, so newest sessions get highest numbers)
            sessions.sort((a, b) => new Date(a.startTime).getTime() - new Date(b.startTime).getTime());
            selectedSession = sessions[sessions.length - 1];
        }
        else {
            try {
                selectedSession = await this.findSession(resumeArg);
            }
            catch (error) {
                // SessionError already has detailed messages - just rethrow
                if (error instanceof SessionError) {
                    throw error;
                }
                // Wrap unexpected errors with context
                throw new Error(`Failed to find session "${resumeArg}": ${error instanceof Error ? error.message : String(error)}`);
            }
        }
        return this.selectSession(selectedSession);
    }
    /**
     * Loads session data for a selected session.
     */
    async selectSession(sessionInfo) {
        const chatsDir = path.join(this.config.storage.getProjectTempDir(), 'chats');
        const sessionPath = path.join(chatsDir, sessionInfo.fileName);
        try {
            const sessionData = JSON.parse(await fs.readFile(sessionPath, 'utf8'));
            const displayInfo = `Session ${sessionInfo.index}: ${sessionInfo.firstUserMessage} (${sessionInfo.messageCount} messages, ${formatRelativeTime(sessionInfo.lastUpdated)})`;
            return {
                sessionPath,
                sessionData,
                displayInfo,
            };
        }
        catch (error) {
            throw new Error(`Failed to load session ${sessionInfo.id}: ${error instanceof Error ? error.message : 'Unknown error'}`);
        }
    }
}
/**
 * Converts session/conversation data into UI history and Gemini client history formats.
 */
export function convertSessionToHistoryFormats(messages) {
    const uiHistory = [];
    for (const msg of messages) {
        // Add the message only if it has content
        const displayContentString = msg.displayContent
            ? partListUnionToString(msg.displayContent)
            : undefined;
        const contentString = partListUnionToString(msg.content);
        const uiText = displayContentString || contentString;
        if (uiText.trim()) {
            let messageType;
            switch (msg.type) {
                case 'user':
                    messageType = MessageType.USER;
                    break;
                case 'info':
                    messageType = MessageType.INFO;
                    break;
                case 'error':
                    messageType = MessageType.ERROR;
                    break;
                case 'warning':
                    messageType = MessageType.WARNING;
                    break;
                case 'gemini':
                    messageType = MessageType.GEMINI;
                    break;
                default:
                    checkExhaustive(msg);
                    messageType = MessageType.GEMINI;
                    break;
            }
            uiHistory.push({
                type: messageType,
                text: uiText,
            });
        }
        // Add tool calls if present
        if (msg.type !== 'user' &&
            'toolCalls' in msg &&
            msg.toolCalls &&
            msg.toolCalls.length > 0) {
            uiHistory.push({
                type: 'tool_group',
                tools: msg.toolCalls.map((tool) => ({
                    callId: tool.id,
                    name: tool.displayName || tool.name,
                    description: tool.description || '',
                    renderOutputAsMarkdown: tool.renderOutputAsMarkdown ?? true,
                    status: tool.status === 'success'
                        ? ToolCallStatus.Success
                        : ToolCallStatus.Error,
                    resultDisplay: tool.resultDisplay,
                    confirmationDetails: undefined,
                })),
            });
        }
    }
    // Convert to Gemini client history format
    const clientHistory = [];
    for (const msg of messages) {
        // Skip system/error messages and user slash commands
        if (msg.type === 'info' || msg.type === 'error' || msg.type === 'warning') {
            continue;
        }
        if (msg.type === 'user') {
            // Skip user slash commands
            const contentString = partListUnionToString(msg.content);
            if (contentString.trim().startsWith('/') ||
                contentString.trim().startsWith('?')) {
                continue;
            }
            // Add regular user message
            clientHistory.push({
                role: 'user',
                parts: Array.isArray(msg.content)
                    ? msg.content
                    : [{ text: contentString }],
            });
        }
        else if (msg.type === 'gemini') {
            // Handle Gemini messages with potential tool calls
            const hasToolCalls = msg.toolCalls && msg.toolCalls.length > 0;
            if (hasToolCalls) {
                // Create model message with function calls
                const modelParts = [];
                // Add text content if present
                const contentString = partListUnionToString(msg.content);
                if (msg.content && contentString.trim()) {
                    modelParts.push({ text: contentString });
                }
                // Add function calls
                for (const toolCall of msg.toolCalls) {
                    modelParts.push({
                        functionCall: {
                            name: toolCall.name,
                            args: toolCall.args,
                            ...(toolCall.id && { id: toolCall.id }),
                        },
                    });
                }
                clientHistory.push({
                    role: 'model',
                    parts: modelParts,
                });
                // Create single function response message with all tool call responses
                const functionResponseParts = [];
                for (const toolCall of msg.toolCalls) {
                    if (toolCall.result) {
                        // Convert PartListUnion result to function response format
                        let responseData;
                        if (typeof toolCall.result === 'string') {
                            responseData = {
                                functionResponse: {
                                    id: toolCall.id,
                                    name: toolCall.name,
                                    response: {
                                        output: toolCall.result,
                                    },
                                },
                            };
                        }
                        else if (Array.isArray(toolCall.result)) {
                            // toolCall.result is an array containing properly formatted
                            // function responses
                            functionResponseParts.push(...toolCall.result);
                            continue;
                        }
                        else {
                            // Fallback for non-array results
                            responseData = toolCall.result;
                        }
                        functionResponseParts.push(responseData);
                    }
                }
                // Only add user message if we have function responses
                if (functionResponseParts.length > 0) {
                    clientHistory.push({
                        role: 'user',
                        parts: functionResponseParts,
                    });
                }
            }
            else {
                // Regular Gemini message without tool calls
                const contentString = partListUnionToString(msg.content);
                if (msg.content && contentString.trim()) {
                    clientHistory.push({
                        role: 'model',
                        parts: [{ text: contentString }],
                    });
                }
            }
        }
    }
    return {
        uiHistory,
        clientHistory,
    };
}
//# sourceMappingURL=sessionUtils.js.map