import fs from 'node:fs';
import fsp from 'node:fs/promises';
import path from 'node:path';
import readline from 'node:readline';
import { createNormalizedMessage, generateMessageId, readObjectRecord } from '../../../../shared/utils.js';
import { sessionsDb } from '../../../../modules/database/index.js';
const PROVIDER = 'claude';
async function parseAgentTools(filePath) {
    const tools = [];
    try {
        const fileStream = fs.createReadStream(filePath);
        const rl = readline.createInterface({
            input: fileStream,
            crlfDelay: Infinity,
        });
        for await (const line of rl) {
            if (!line.trim()) {
                continue;
            }
            try {
                const entry = JSON.parse(line);
                if (entry.message?.role === 'assistant' && Array.isArray(entry.message?.content)) {
                    for (const part of entry.message.content) {
                        if (part.type === 'tool_use') {
                            tools.push({
                                toolId: part.id,
                                toolName: part.name,
                                toolInput: part.input,
                                timestamp: entry.timestamp,
                            });
                        }
                    }
                }
                if (entry.message?.role === 'user' && Array.isArray(entry.message?.content)) {
                    for (const part of entry.message.content) {
                        if (part.type !== 'tool_result') {
                            continue;
                        }
                        const tool = tools.find((candidate) => candidate.toolId === part.tool_use_id);
                        if (!tool) {
                            continue;
                        }
                        tool.toolResult = {
                            content: typeof part.content === 'string'
                                ? part.content
                                : Array.isArray(part.content)
                                    ? part.content
                                        .map((contentPart) => contentPart?.text || '')
                                        .join('\n')
                                    : JSON.stringify(part.content),
                            isError: Boolean(part.is_error),
                        };
                    }
                }
            }
            catch {
                // Skip malformed lines that can happen during concurrent writes.
            }
        }
    }
    catch (error) {
        const message = error instanceof Error ? error.message : String(error);
        console.warn(`Error parsing agent file ${filePath}:`, message);
    }
    return tools;
}
async function getSessionMessages(sessionId, limit, offset) {
    try {
        const jsonLPath = sessionsDb.getSessionById(sessionId)?.jsonl_path;
        if (!jsonLPath) {
            return { messages: [], total: 0, hasMore: false };
        }
        const projectDir = path.dirname(jsonLPath);
        const files = await fsp.readdir(projectDir);
        const agentFiles = files.filter((file) => file.endsWith('.jsonl') && file.startsWith('agent-'));
        const messages = [];
        const agentToolsCache = new Map();
        const fileStream = fs.createReadStream(jsonLPath);
        const rl = readline.createInterface({
            input: fileStream,
            crlfDelay: Infinity,
        });
        for await (const line of rl) {
            if (!line.trim()) {
                continue;
            }
            try {
                const entry = JSON.parse(line);
                if (entry.sessionId === sessionId) {
                    messages.push(entry);
                }
            }
            catch {
                // Skip malformed JSONL lines that can happen during concurrent writes.
            }
        }
        const agentIds = new Set();
        for (const message of messages) {
            const agentId = message.toolUseResult?.agentId;
            if (agentId) {
                agentIds.add(String(agentId));
            }
        }
        for (const agentId of agentIds) {
            const agentFileName = `agent-${agentId}.jsonl`;
            if (!agentFiles.includes(agentFileName)) {
                continue;
            }
            const agentFilePath = path.join(projectDir, agentFileName);
            const tools = await parseAgentTools(agentFilePath);
            agentToolsCache.set(agentId, tools);
        }
        for (const message of messages) {
            const agentId = message.toolUseResult?.agentId;
            if (!agentId) {
                continue;
            }
            const agentTools = agentToolsCache.get(String(agentId));
            if (agentTools && agentTools.length > 0) {
                message.subagentTools = agentTools;
            }
        }
        const sortedMessages = messages.sort((a, b) => new Date(a.timestamp || 0).getTime() - new Date(b.timestamp || 0).getTime());
        const total = sortedMessages.length;
        if (limit === null) {
            return sortedMessages;
        }
        const startIndex = Math.max(0, total - offset - limit);
        const endIndex = total - offset;
        const paginatedMessages = sortedMessages.slice(startIndex, endIndex);
        const hasMore = startIndex > 0;
        return {
            messages: paginatedMessages,
            total,
            hasMore,
            offset,
            limit,
        };
    }
    catch (error) {
        console.error(`Error reading messages for session ${sessionId}:`, error);
        return limit === null ? [] : { messages: [], total: 0, hasMore: false };
    }
}
/**
 * Claude writes a mix of truly internal transcript rows and "UI-hidden" local
 * command artifacts into the same JSONL stream.
 *
 * Important distinction:
 * - system reminders / caveats / interruption banners should stay hidden
 * - local command payloads (`<command-name>...`) and stdout wrappers
 *   (`<local-command-stdout>...`) should be remapped into normal chat messages
 *   instead of being discarded as internal content
 */
const INTERNAL_CONTENT_PREFIXES = [
    '<system-reminder>',
    'Caveat:',
    '[Request interrupted',
];
function isInternalContent(content) {
    return INTERNAL_CONTENT_PREFIXES.some((prefix) => content.startsWith(prefix));
}
/**
 * Claude wraps local slash-command metadata in lightweight XML-like tags inside
 * a plain string payload. We intentionally parse only the small tag surface we
 * care about instead of introducing a generic XML parser for untrusted history.
 */
function extractTaggedContent(content, tagName) {
    const escapedTagName = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    const match = new RegExp(`<${escapedTagName}>([\\s\\S]*?)<\\/${escapedTagName}>`).exec(content);
    return match ? match[1] : null;
}
/**
 * Converts Claude's hidden local command wrapper into structured metadata.
 *
 * The three tags often coexist in one string payload. Returning `null` lets the
 * normal text path continue untouched for unrelated messages.
 */
function parseLocalCommandPayload(content) {
    const commandName = extractTaggedContent(content, 'command-name');
    const commandMessage = extractTaggedContent(content, 'command-message');
    const commandArgs = extractTaggedContent(content, 'command-args');
    if (commandName === null && commandMessage === null && commandArgs === null) {
        return null;
    }
    return {
        commandName: commandName ?? '',
        commandMessage: commandMessage ?? '',
        commandArgs: commandArgs ?? '',
    };
}
/**
 * Produces the short user-visible command string that should appear in chat.
 *
 * We prefer the slash-prefixed command name because that most closely matches
 * what the user actually typed, and only fall back to the message body when the
 * command name is unavailable in older transcript variants.
 */
function buildLocalCommandDisplayText(payload) {
    const commandName = payload.commandName.trim();
    const commandMessage = payload.commandMessage.trim();
    const commandArgs = payload.commandArgs.trim();
    const baseCommand = commandName || commandMessage;
    if (!baseCommand) {
        return '';
    }
    return commandArgs ? `${baseCommand} ${commandArgs}` : baseCommand;
}
/**
 * Claude local-command stdout may contain ANSI styling codes because it was
 * captured from the terminal. The web chat should receive readable plain text.
 */
function stripAnsiFormatting(text) {
    return text.replace(/\u001B\[[0-9;?]*[ -/]*[@-~]/g, '');
}
export class ClaudeSessionsProvider {
    /**
     * Normalizes one Claude JSONL entry or live SDK stream event into the shared
     * message shape consumed by REST and WebSocket clients.
     */
    normalizeMessage(rawMessage, sessionId) {
        const raw = readObjectRecord(rawMessage);
        if (!raw) {
            return [];
        }
        if (raw.type === 'content_block_delta' && raw.delta?.text) {
            return [createNormalizedMessage({ kind: 'stream_delta', content: raw.delta.text, sessionId, provider: PROVIDER })];
        }
        if (raw.type === 'content_block_stop') {
            return [createNormalizedMessage({ kind: 'stream_end', sessionId, provider: PROVIDER })];
        }
        const messages = [];
        const ts = raw.timestamp || new Date().toISOString();
        const baseId = raw.uuid || generateMessageId('claude');
        if (raw.message?.role === 'user' && raw.message?.content && raw.isMeta !== true) {
            if (Array.isArray(raw.message.content)) {
                for (let partIndex = 0; partIndex < raw.message.content.length; partIndex++) {
                    const part = raw.message.content[partIndex];
                    if (part.type === 'tool_result') {
                        messages.push(createNormalizedMessage({
                            id: `${baseId}_tr_${part.tool_use_id}`,
                            sessionId,
                            timestamp: ts,
                            provider: PROVIDER,
                            kind: 'tool_result',
                            toolId: part.tool_use_id,
                            content: typeof part.content === 'string' ? part.content : JSON.stringify(part.content),
                            isError: Boolean(part.is_error),
                            subagentTools: raw.subagentTools,
                            toolUseResult: raw.toolUseResult,
                        }));
                    }
                    else if (part.type === 'text') {
                        const text = part.text || '';
                        if (text && !isInternalContent(text)) {
                            messages.push(createNormalizedMessage({
                                id: `${baseId}_text_${partIndex}`,
                                sessionId,
                                timestamp: ts,
                                provider: PROVIDER,
                                kind: 'text',
                                role: 'user',
                                content: text,
                            }));
                        }
                    }
                }
                if (messages.length === 0) {
                    const textParts = raw.message.content
                        .filter((part) => part.type === 'text')
                        .map((part) => part.text)
                        .filter(Boolean)
                        .join('\n');
                    if (textParts && !isInternalContent(textParts)) {
                        messages.push(createNormalizedMessage({
                            id: `${baseId}_text`,
                            sessionId,
                            timestamp: ts,
                            provider: PROVIDER,
                            kind: 'text',
                            role: 'user',
                            content: textParts,
                        }));
                    }
                }
            }
            else if (typeof raw.message.content === 'string') {
                const text = raw.message.content;
                /**
                 * Claude stores compact summaries as synthetic "user" rows so the CLI
                 * can resume the next session turn with the summary in-context.
                 *
                 * For the web UI this is much more useful as assistant-authored summary
                 * text; otherwise it is both filtered by the generic internal-prefix
                 * check and visually mislabeled as a user message.
                 */
                if (raw.isCompactSummary === true && text.trim()) {
                    messages.push(createNormalizedMessage({
                        id: baseId,
                        sessionId,
                        timestamp: ts,
                        provider: PROVIDER,
                        kind: 'text',
                        role: 'assistant',
                        content: text,
                        isCompactSummary: true,
                    }));
                    return messages;
                }
                /**
                 * Local slash commands are serialized as tagged text even though they
                 * are semantically a user action. Expose the parsed fields to the
                 * frontend and emit a plain user-visible command string so the command
                 * no longer disappears from history.
                 */
                const localCommandPayload = parseLocalCommandPayload(text);
                if (localCommandPayload) {
                    const displayText = buildLocalCommandDisplayText(localCommandPayload);
                    if (displayText) {
                        messages.push(createNormalizedMessage({
                            id: baseId,
                            sessionId,
                            timestamp: ts,
                            provider: PROVIDER,
                            kind: 'text',
                            role: 'user',
                            content: displayText,
                            commandName: localCommandPayload.commandName,
                            commandMessage: localCommandPayload.commandMessage,
                            commandArgs: localCommandPayload.commandArgs,
                            isLocalCommand: true,
                        }));
                    }
                    return messages;
                }
                /**
                 * Local command stdout is also written as a "user" row in Claude's
                 * transcript, but it is terminal output produced in response to the
                 * command. Re-label it as assistant text so the chat transcript matches
                 * the actual conversational flow seen by the user.
                 */
                const localCommandStdout = extractTaggedContent(text, 'local-command-stdout');
                if (localCommandStdout !== null) {
                    const stdoutText = stripAnsiFormatting(localCommandStdout).trim();
                    if (stdoutText) {
                        messages.push(createNormalizedMessage({
                            id: baseId,
                            sessionId,
                            timestamp: ts,
                            provider: PROVIDER,
                            kind: 'text',
                            role: 'assistant',
                            content: stdoutText,
                            isLocalCommandStdout: true,
                        }));
                    }
                    return messages;
                }
                if (text && !isInternalContent(text)) {
                    messages.push(createNormalizedMessage({
                        id: baseId,
                        sessionId,
                        timestamp: ts,
                        provider: PROVIDER,
                        kind: 'text',
                        role: 'user',
                        content: text,
                    }));
                }
            }
            return messages;
        }
        if (raw.type === 'thinking' && raw.message?.content) {
            messages.push(createNormalizedMessage({
                id: baseId,
                sessionId,
                timestamp: ts,
                provider: PROVIDER,
                kind: 'thinking',
                content: raw.message.content,
            }));
            return messages;
        }
        if (raw.type === 'tool_use' && raw.toolName) {
            messages.push(createNormalizedMessage({
                id: baseId,
                sessionId,
                timestamp: ts,
                provider: PROVIDER,
                kind: 'tool_use',
                toolName: raw.toolName,
                toolInput: raw.toolInput,
                toolId: raw.toolCallId || baseId,
            }));
            return messages;
        }
        if (raw.type === 'tool_result') {
            messages.push(createNormalizedMessage({
                id: baseId,
                sessionId,
                timestamp: ts,
                provider: PROVIDER,
                kind: 'tool_result',
                toolId: raw.toolCallId || '',
                content: raw.output || '',
                isError: false,
            }));
            return messages;
        }
        if (raw.message?.role === 'assistant' && raw.message?.content) {
            if (Array.isArray(raw.message.content)) {
                let partIndex = 0;
                for (const part of raw.message.content) {
                    if (part.type === 'text' && part.text) {
                        messages.push(createNormalizedMessage({
                            id: `${baseId}_${partIndex}`,
                            sessionId,
                            timestamp: ts,
                            provider: PROVIDER,
                            kind: 'text',
                            role: 'assistant',
                            content: part.text,
                        }));
                    }
                    else if (part.type === 'tool_use') {
                        messages.push(createNormalizedMessage({
                            id: `${baseId}_${partIndex}`,
                            sessionId,
                            timestamp: ts,
                            provider: PROVIDER,
                            kind: 'tool_use',
                            toolName: part.name,
                            toolInput: part.input,
                            toolId: part.id,
                        }));
                    }
                    else if (part.type === 'thinking' && part.thinking) {
                        messages.push(createNormalizedMessage({
                            id: `${baseId}_${partIndex}`,
                            sessionId,
                            timestamp: ts,
                            provider: PROVIDER,
                            kind: 'thinking',
                            content: part.thinking,
                        }));
                    }
                    partIndex++;
                }
            }
            else if (typeof raw.message.content === 'string') {
                messages.push(createNormalizedMessage({
                    id: baseId,
                    sessionId,
                    timestamp: ts,
                    provider: PROVIDER,
                    kind: 'text',
                    role: 'assistant',
                    content: raw.message.content,
                }));
            }
            return messages;
        }
        return messages;
    }
    /**
     * Loads Claude JSONL history for a project/session and returns normalized
     * messages, preserving the existing pagination behavior from projects.js.
     */
    async fetchHistory(sessionId, options = {}) {
        const { limit = null, offset = 0 } = options;
        let result;
        try {
            // Load full history first so `total` reflects frontend-normalized messages,
            // not raw JSONL records.
            result = await getSessionMessages(sessionId, null, 0);
        }
        catch (error) {
            const message = error instanceof Error ? error.message : String(error);
            console.warn(`[ClaudeProvider] Failed to load session ${sessionId}:`, message);
            return { messages: [], total: 0, hasMore: false, offset: 0, limit: null };
        }
        const rawMessages = Array.isArray(result) ? result : (result.messages || []);
        const toolResultMap = new Map();
        for (const raw of rawMessages) {
            if (raw.message?.role === 'user' && Array.isArray(raw.message?.content)) {
                for (const part of raw.message.content) {
                    if (part.type === 'tool_result' && part.tool_use_id) {
                        toolResultMap.set(part.tool_use_id, {
                            content: part.content,
                            isError: Boolean(part.is_error),
                            subagentTools: raw.subagentTools,
                            toolUseResult: raw.toolUseResult,
                        });
                    }
                }
            }
        }
        const normalized = [];
        for (const raw of rawMessages) {
            normalized.push(...this.normalizeMessage(raw, sessionId));
        }
        // ─── Sidecar attachment reader ─────────────────────────────────────
        // Read the sidecar .attachments.jsonl and merge attachment data into
        // user text messages in sequence order. The seq counter matches the
        // Nth user text message with the Nth sidecar entry.
        try {
            const jsonLPath = sessionsDb.getSessionById(sessionId)?.jsonl_path;
            if (jsonLPath) {
                const attachmentsDir = path.join(path.dirname(jsonLPath), 'attachments');
                const sidecarPath = path.join(attachmentsDir, `${sessionId}.attachments.jsonl`);
                const sidecarContent = await fsp.readFile(sidecarPath, 'utf8').catch(() => null);
                if (sidecarContent) {
                    const entries = sidecarContent.trim().split('\n').filter(Boolean).map(line => {
                        try {
                            return JSON.parse(line);
                        }
                        catch {
                            return null;
                        }
                    }).filter(Boolean);
                    let userMsgIndex = 0;
                    for (const msg of normalized) {
                        if (msg.kind === 'text' && msg.role === 'user') {
                            const entry = entries[userMsgIndex];
                            if (entry && (entry.images?.length > 0 || entry.files?.length > 0)) {
                                msg.images = (entry.images || []).map((img) => ({
                                    data: img.data,
                                    name: img.name,
                                    size: img.size,
                                    mimeType: img.mimeType,
                                }));
                                msg.files = (entry.files || []).map((f) => ({
                                    data: f.data,
                                    name: f.name,
                                    size: f.size,
                                    mimeType: f.mimeType,
                                }));
                            }
                            userMsgIndex++;
                        }
                    }
                }
            }
        }
        catch (error) {
            console.warn(`[ClaudeProvider] Failed to read attachment sidecar for ${sessionId}:`, error);
        }
        for (const msg of normalized) {
            if (msg.kind === 'tool_use' && msg.toolId && toolResultMap.has(msg.toolId)) {
                const toolResult = toolResultMap.get(msg.toolId);
                if (!toolResult) {
                    continue;
                }
                msg.toolResult = {
                    content: typeof toolResult.content === 'string'
                        ? toolResult.content
                        : JSON.stringify(toolResult.content),
                    isError: toolResult.isError,
                    toolUseResult: toolResult.toolUseResult,
                };
                msg.subagentTools = toolResult.subagentTools;
            }
        }
        const totalNormalized = normalized.length;
        let total = 0;
        for (const msg of normalized) {
            if (msg.kind !== 'tool_result') {
                total += 1;
            }
        }
        const normalizedOffset = Math.max(0, offset);
        const normalizedLimit = limit === null ? null : Math.max(0, limit);
        const messages = normalizedLimit === null
            ? normalized
            : normalized.slice(Math.max(0, totalNormalized - normalizedOffset - normalizedLimit), Math.max(0, totalNormalized - normalizedOffset));
        const hasMore = normalizedLimit === null
            ? false
            : Math.max(0, totalNormalized - normalizedOffset - normalizedLimit) > 0;
        return {
            messages,
            total,
            hasMore,
            offset: normalizedOffset,
            limit: normalizedLimit,
        };
    }
}
//# sourceMappingURL=claude-sessions.provider.js.map