/**
 * Claude Code Session Repository
 *
 * Infrastructure implementation of IAgentSessionRepository for Claude Code.
 * Reads JSONL session files from ~/.claude/projects/ using a lazy stat-then-parse
 * strategy for performance: stat all files in parallel for mtime-based sorting,
 * then fully parse only the top-N files needed for the list view.
 *
 * File structure:
 *   ~/.claude/projects/<encoded-project-path>/<uuid>.jsonl
 *
 * Subagent sessions stored in subdirectories are excluded by only reading
 * depth-1 .jsonl files from each project directory.
 */
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
import * as fs from 'node:fs/promises';
import * as os from 'node:os';
import * as path from 'node:path';
import { injectable } from 'tsyringe';
let ClaudeCodeSessionRepository = class ClaudeCodeSessionRepository {
    basePath;
    constructor(basePath = path.join(os.homedir(), '.claude', 'projects')) {
        this.basePath = basePath;
    }
    isSupported() {
        return true;
    }
    async list(options) {
        const limit = options?.limit ?? 20;
        const filterPath = options?.projectPath;
        // When filtering by path, use the directory naming convention to scan only the
        // matching project directory instead of all 100+ directories. Claude Code encodes
        // project paths as directory names by replacing '/' with '-'.
        const fileInfos = filterPath
            ? await this.collectSessionFilesForPath(filterPath)
            : await this.collectSessionFiles();
        fileInfos.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
        // Apply limit before full parsing
        const toParse = limit > 0 ? fileInfos.slice(0, limit) : fileInfos;
        const parseResults = await Promise.allSettled(toParse.map((fi) => this.parseSessionFile(fi, { includeMessages: false })));
        const sessions = [];
        for (const result of parseResults) {
            if (result.status === 'fulfilled' && result.value !== null) {
                sessions.push(result.value);
            }
        }
        return sessions;
    }
    async findById(id, options) {
        const messageLimit = options?.messageLimit ?? 20;
        const match = await this.findSessionFile(id);
        if (match === null)
            return null;
        try {
            const stat = await fs.stat(match.filePath);
            const fileInfo = {
                id: match.resolvedId,
                filePath: match.filePath,
                mtime: stat.mtime,
            };
            return await this.parseSessionFile(fileInfo, { includeMessages: true, messageLimit });
        }
        catch {
            return null;
        }
    }
    /**
     * Collect session files only from the directory matching the given project path.
     * Claude Code encodes project paths as directory names by replacing '/', '\', and '.'
     * with '-'. e.g. /home/user/.shep/repos/abc/wt/feat-x → -home-user--shep-repos-abc-wt-feat-x
     * This avoids scanning all 100+ project directories.
     */
    async collectSessionFilesForPath(projectPath) {
        // Normalize: resolve ~ and convert path separators
        const normalizedPath = projectPath.startsWith('~')
            ? path.join(os.homedir(), projectPath.slice(1))
            : projectPath;
        // Claude Code replaces '/', '\', and '.' with '-' in directory names
        const dirName = normalizedPath.replace(/[/\\.]/g, '-');
        const projectDir = path.join(this.basePath, dirName);
        try {
            return await this.collectDepthOneJsonlFiles(projectDir);
        }
        catch {
            // Directory doesn't exist — no sessions for this path
            return [];
        }
    }
    /** Collect all depth-1 .jsonl session files with mtime from all project directories */
    async collectSessionFiles() {
        let entries;
        try {
            entries = await fs.readdir(this.basePath, { withFileTypes: true, encoding: 'utf-8' });
        }
        catch {
            return [];
        }
        const projectDirs = entries
            .filter((e) => e.isDirectory())
            .map((e) => path.join(this.basePath, e.name));
        const results = await Promise.allSettled(projectDirs.map((dir) => this.collectDepthOneJsonlFiles(dir)));
        const fileInfos = [];
        for (const result of results) {
            if (result.status === 'fulfilled') {
                fileInfos.push(...result.value);
            }
        }
        return fileInfos;
    }
    /** Collect depth-1 .jsonl files from a single project directory with stat for mtime */
    async collectDepthOneJsonlFiles(projectDir) {
        const entries = await fs.readdir(projectDir, { withFileTypes: true, encoding: 'utf-8' });
        const jsonlFiles = entries.filter((e) => e.isFile() && e.name.endsWith('.jsonl'));
        const statResults = await Promise.allSettled(jsonlFiles.map(async (e) => {
            const filePath = path.join(projectDir, e.name);
            const stat = await fs.stat(filePath);
            const id = e.name.slice(0, -'.jsonl'.length);
            return { id, filePath, mtime: stat.mtime };
        }));
        const fileInfos = [];
        for (const result of statResults) {
            if (result.status === 'fulfilled') {
                fileInfos.push(result.value);
            }
        }
        return fileInfos;
    }
    /**
     * Find a session file by exact or prefix ID match, scanning all project directories.
     * Supports prefix matching so users can pass truncated IDs (e.g. first 8 chars).
     * Returns the match info or null if not found / ambiguous.
     */
    async findSessionFile(id) {
        let entries;
        try {
            entries = await fs.readdir(this.basePath, { withFileTypes: true, encoding: 'utf-8' });
        }
        catch {
            return null;
        }
        const projectDirs = entries
            .filter((e) => e.isDirectory())
            .map((e) => path.join(this.basePath, e.name));
        // Try exact match first (fast path)
        for (const dir of projectDirs) {
            const filePath = path.join(dir, `${id}.jsonl`);
            try {
                await fs.access(filePath);
                return { filePath, resolvedId: id };
            }
            catch {
                // Not in this directory
            }
        }
        // Fall back to prefix match
        const matches = [];
        for (const dir of projectDirs) {
            let dirEntries;
            try {
                dirEntries = await fs.readdir(dir, { withFileTypes: true, encoding: 'utf-8' });
            }
            catch {
                continue;
            }
            for (const e of dirEntries) {
                if (e.isFile() && e.name.endsWith('.jsonl') && e.name.startsWith(id)) {
                    const resolvedId = e.name.slice(0, -'.jsonl'.length);
                    matches.push({ filePath: path.join(dir, e.name), resolvedId });
                }
            }
        }
        if (matches.length === 1) {
            return matches[0];
        }
        // No match or ambiguous (multiple matches)
        return null;
    }
    /**
     * Parse a JSONL session file into an AgentSession.
     *
     * Throws on any JSON parse failure so the caller can skip the file.
     */
    async parseSessionFile(fileInfo, options) {
        const content = await fs.readFile(fileInfo.filePath, 'utf-8');
        const lines = content.split('\n').filter((l) => l.trim().length > 0);
        let cwd;
        let firstMessageAt;
        let lastMessageAt;
        let preview;
        let messageCount = 0;
        const messages = [];
        // Extra metadata tracking
        let cliVersion;
        let gitBranch;
        let permissionMode;
        let userType;
        const toolUsage = {};
        let userMessageCount = 0;
        let assistantMessageCount = 0;
        for (const line of lines) {
            // JSON.parse throws on invalid JSON — propagates to caller which skips the file
            const entry = JSON.parse(line);
            if (!cwd && typeof entry.cwd === 'string') {
                cwd = entry.cwd;
            }
            if (!cliVersion && typeof entry.version === 'string') {
                cliVersion = entry.version;
            }
            if (!gitBranch && typeof entry.gitBranch === 'string') {
                gitBranch = entry.gitBranch;
            }
            if (!permissionMode && typeof entry.permissionMode === 'string') {
                permissionMode = entry.permissionMode;
            }
            if (!userType && typeof entry.userType === 'string') {
                userType = entry.userType;
            }
            if (entry.type === 'user' || entry.type === 'assistant') {
                const message = entry.message;
                const role = message?.role;
                if (role === 'user' || role === 'assistant') {
                    messageCount++;
                    if (role === 'user')
                        userMessageCount++;
                    if (role === 'assistant')
                        assistantMessageCount++;
                    const timestamp = entry.timestamp ? new Date(entry.timestamp) : fileInfo.mtime;
                    firstMessageAt ??= timestamp;
                    lastMessageAt = timestamp;
                    if (entry.type === 'user' && preview === undefined) {
                        preview = this.extractTextContent(message?.content);
                    }
                    // Track tool usage from assistant messages
                    if (role === 'assistant' && Array.isArray(message?.content)) {
                        for (const block of message.content) {
                            if (block?.type === 'tool_use' && typeof block.name === 'string') {
                                toolUsage[block.name] = (toolUsage[block.name] ?? 0) + 1;
                            }
                        }
                    }
                    if (options.includeMessages) {
                        messages.push({
                            uuid: entry.uuid ?? '',
                            role: role,
                            content: this.extractTextContent(message?.content),
                            timestamp,
                        });
                    }
                }
            }
        }
        if (cwd === undefined) {
            // Could not determine project path — file is too sparse to be useful
            return null;
        }
        let messagesToReturn = messages;
        if (options.includeMessages && options.messageLimit !== undefined && options.messageLimit > 0) {
            messagesToReturn = messages.slice(-options.messageLimit);
        }
        const session = {
            id: fileInfo.id,
            agentType: 'claude-code',
            projectPath: this.abbreviatePath(cwd),
            messageCount,
            createdAt: firstMessageAt ?? fileInfo.mtime,
            updatedAt: lastMessageAt ?? fileInfo.mtime,
        };
        if (preview !== undefined) {
            session.preview = preview;
        }
        if (firstMessageAt !== undefined) {
            session.firstMessageAt = firstMessageAt;
        }
        if (lastMessageAt !== undefined) {
            session.lastMessageAt = lastMessageAt;
        }
        if (options.includeMessages) {
            session.messages = messagesToReturn;
            session.metadata = {
                cliVersion,
                gitBranch,
                permissionMode,
                userType,
                toolUsage,
                userMessageCount,
                assistantMessageCount,
            };
        }
        return session;
    }
    /**
     * Extract plain text from message content.
     * - string content: returned as-is
     * - array content: concatenates all text blocks; falls back to tool_use summary
     */
    extractTextContent(content) {
        if (typeof content === 'string') {
            return content;
        }
        if (Array.isArray(content)) {
            // Collect all text blocks
            const textParts = [];
            const toolNames = [];
            for (const block of content) {
                if (typeof block !== 'object' || block === null)
                    continue;
                const b = block;
                if (b.type === 'text' && typeof b.text === 'string') {
                    textParts.push(b.text);
                }
                else if (b.type === 'tool_use' && typeof b.name === 'string') {
                    toolNames.push(b.name);
                }
            }
            if (textParts.length > 0) {
                return textParts.join('\n');
            }
            // No text blocks — summarize tool usage
            if (toolNames.length > 0) {
                return `[${toolNames.join(', ')}]`;
            }
        }
        return '';
    }
    /** Replace home directory prefix with ~ in a file path */
    abbreviatePath(filePath) {
        const home = os.homedir();
        if (filePath === home)
            return '~';
        if (filePath.startsWith(`${home}${path.sep}`)) {
            return `~${filePath.slice(home.length)}`;
        }
        return filePath;
    }
};
ClaudeCodeSessionRepository = __decorate([
    injectable(),
    __metadata("design:paramtypes", [String])
], ClaudeCodeSessionRepository);
export { ClaudeCodeSessionRepository };
