/**
 * @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';
/**
 * 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'}`);
        }
    }
}
//# sourceMappingURL=sessionUtils.js.map