import fsSync from 'node:fs';
import Database from 'better-sqlite3';
import { createNormalizedMessage, generateMessageId, getOpenCodeDatabasePath, normalizeProviderTimestamp, readObjectRecord, readJsonRecord, readOptionalString, } from '../../../../shared/utils.js';
const PROVIDER = 'opencode';
const openOpenCodeDatabase = () => {
    const dbPath = getOpenCodeDatabasePath();
    if (!fsSync.existsSync(dbPath)) {
        return null;
    }
    return new Database(dbPath, { readonly: true, fileMustExist: true });
};
const formatToolContent = (value) => {
    if (value === undefined || value === null) {
        return '';
    }
    if (typeof value === 'string') {
        return value;
    }
    try {
        return JSON.stringify(value, null, 2);
    }
    catch {
        return String(value);
    }
};
/**
 * OpenCode can persist the first prompt as a JSON string literal inside a text
 * part, for example `"hello"` instead of `hello`. Decode only complete JSON
 * string literals so normal assistant/user prose remains untouched.
 */
const unwrapJsonStringLiteral = (value) => {
    const trimmed = value.trim();
    if (!trimmed.startsWith('"') || !trimmed.endsWith('"')) {
        return value;
    }
    try {
        const parsed = JSON.parse(trimmed);
        return typeof parsed === 'string' ? parsed : value;
    }
    catch {
        return value;
    }
};
const extractText = (value) => {
    if (typeof value === 'string') {
        return unwrapJsonStringLiteral(value);
    }
    const record = readObjectRecord(value);
    const text = readOptionalString(record?.text)
        ?? readOptionalString(record?.content)
        ?? '';
    return unwrapJsonStringLiteral(text);
};
const hasUserRole = (value) => {
    const record = readObjectRecord(value);
    return readOptionalString(record?.role) === 'user';
};
const isUserTextEcho = (raw) => {
    return readOptionalString(raw.role) === 'user'
        || hasUserRole(raw.message)
        || hasUserRole(raw.part);
};
const buildTokenUsage = (totals) => {
    if (!totals) {
        return undefined;
    }
    const inputTokens = totals.inputTokens;
    const outputTokens = totals.outputTokens;
    const cacheReadTokens = totals.cacheReadTokens;
    const cacheCreationTokens = totals.cacheCreationTokens;
    const reasoningTokens = totals.reasoningTokens;
    const used = inputTokens + outputTokens + cacheReadTokens + cacheCreationTokens + reasoningTokens;
    if (used <= 0) {
        return undefined;
    }
    return {
        used,
        total: used,
        inputTokens,
        outputTokens,
        cacheReadTokens,
        cacheCreationTokens,
    };
};
/**
 * OpenCode stores per-message token counts on assistant `message.data` objects
 * (see MessageV2.Assistant). Older DBs also had session-level counters; this
 * matches current `opencode.db` layouts that only persist message JSON.
 */
const aggregateOpenCodeSessionTokenUsage = (db, sessionId) => {
    const rows = db.prepare('SELECT data FROM message WHERE session_id = ?').all(sessionId);
    let inputTokens = 0;
    let outputTokens = 0;
    let cacheReadTokens = 0;
    let cacheCreationTokens = 0;
    let reasoningTokens = 0;
    for (const row of rows) {
        const info = readJsonRecord(row.data);
        if (readOptionalString(info?.role) !== 'assistant') {
            continue;
        }
        const tokens = readObjectRecord(info?.tokens);
        if (!tokens) {
            continue;
        }
        inputTokens += Number(tokens.input ?? 0);
        outputTokens += Number(tokens.output ?? 0);
        reasoningTokens += Number(tokens.reasoning ?? 0);
        const cache = readObjectRecord(tokens.cache);
        cacheReadTokens += Number(cache?.read ?? 0);
        cacheCreationTokens += Number(cache?.write ?? 0);
    }
    return buildTokenUsage({
        inputTokens,
        outputTokens,
        cacheReadTokens,
        cacheCreationTokens,
        reasoningTokens,
    });
};
export class OpenCodeSessionsProvider {
    /**
     * Normalizes live `opencode run --format json` events into frontend messages.
     */
    normalizeMessage(rawMessage, sessionId) {
        const raw = readObjectRecord(rawMessage);
        if (!raw) {
            return [];
        }
        const type = readOptionalString(raw.type) ?? readOptionalString(raw.event);
        const eventSessionId = readOptionalString(raw.sessionID) ?? readOptionalString(raw.sessionId) ?? sessionId;
        const timestamp = normalizeProviderTimestamp(raw.time ?? raw.timestamp);
        const baseId = readOptionalString(raw.id)
            ?? readOptionalString(raw.messageID)
            ?? generateMessageId('opencode');
        if (type === 'text') {
            // The client already renders an optimistic user bubble, so provider user
            // echoes must not be streamed back as assistant text.
            if (isUserTextEcho(raw)) {
                return [];
            }
            const content = extractText(raw.text ?? raw.delta ?? raw.message);
            if (!content.trim()) {
                return [];
            }
            return [createNormalizedMessage({
                    id: baseId,
                    sessionId: eventSessionId,
                    timestamp,
                    provider: PROVIDER,
                    kind: 'stream_delta',
                    content,
                })];
        }
        if (type === 'reasoning') {
            const content = extractText(raw.text ?? raw.delta ?? raw.message);
            if (!content.trim()) {
                return [];
            }
            return [createNormalizedMessage({
                    id: baseId,
                    sessionId: eventSessionId,
                    timestamp,
                    provider: PROVIDER,
                    kind: 'thinking',
                    content,
                })];
        }
        if (type === 'tool_use') {
            const toolName = readOptionalString(raw.tool) ?? readOptionalString(raw.name) ?? 'Tool';
            const toolId = readOptionalString(raw.callID) ?? readOptionalString(raw.toolCallId) ?? baseId;
            const toolMessage = createNormalizedMessage({
                id: baseId,
                sessionId: eventSessionId,
                timestamp,
                provider: PROVIDER,
                kind: 'tool_use',
                toolName,
                toolInput: raw.input ?? raw.arguments ?? {},
                toolId,
            });
            if (raw.output !== undefined || raw.error !== undefined) {
                toolMessage.toolResult = {
                    content: formatToolContent(raw.output ?? raw.error),
                    isError: raw.error !== undefined,
                };
            }
            return [toolMessage];
        }
        if (type === 'error') {
            return [createNormalizedMessage({
                    id: baseId,
                    sessionId: eventSessionId,
                    timestamp,
                    provider: PROVIDER,
                    kind: 'error',
                    content: readOptionalString(raw.error) ?? readOptionalString(raw.message) ?? 'Unknown OpenCode error',
                })];
        }
        if (type === 'step_finish') {
            return [createNormalizedMessage({
                    id: baseId,
                    sessionId: eventSessionId,
                    timestamp,
                    provider: PROVIDER,
                    kind: 'stream_end',
                })];
        }
        return [];
    }
    /**
     * Loads OpenCode history from the shared SQLite session database.
     */
    async fetchHistory(sessionId, options = {}) {
        const { limit = null, offset = 0 } = options;
        const db = openOpenCodeDatabase();
        if (!db) {
            return { messages: [], total: 0, hasMore: false, offset: 0, limit: null };
        }
        try {
            const rows = db.prepare(`
        SELECT
          m.id AS message_id,
          m.time_created AS message_time_created,
          m.data AS message_data,
          p.id AS part_id,
          p.time_created AS part_time_created,
          p.data AS part_data
        FROM message m
        LEFT JOIN part p
          ON p.session_id = m.session_id
         AND p.message_id = m.id
        WHERE m.session_id = ?
        ORDER BY
          COALESCE(m.time_created, 0),
          m.id,
          COALESCE(p.time_created, 0),
          p.id
      `).all(sessionId);
            const normalized = this.normalizeHistoryRows(rows, sessionId);
            const tokenUsage = aggregateOpenCodeSessionTokenUsage(db, sessionId);
            const normalizedOffset = Math.max(0, offset);
            const normalizedLimit = limit === null ? null : Math.max(0, limit);
            const total = normalized.length;
            const messages = normalizedLimit === null
                ? normalized
                : normalized.slice(Math.max(0, total - normalizedOffset - normalizedLimit), Math.max(0, total - normalizedOffset));
            return {
                messages,
                total,
                hasMore: normalizedLimit === null
                    ? false
                    : Math.max(0, total - normalizedOffset - normalizedLimit) > 0,
                offset: normalizedOffset,
                limit: normalizedLimit,
                tokenUsage,
            };
        }
        catch (error) {
            const message = error instanceof Error ? error.message : String(error);
            console.warn(`[OpenCodeProvider] Failed to load session ${sessionId}:`, message);
            return { messages: [], total: 0, hasMore: false, offset: 0, limit: null };
        }
        finally {
            db.close();
        }
    }
    normalizeHistoryRows(rows, sessionId) {
        const normalized = [];
        const emittedMessageErrors = new Set();
        for (const row of rows) {
            const timestamp = normalizeProviderTimestamp(row.part_time_created ?? row.message_time_created);
            const baseId = `${row.message_id}_${row.part_id ?? normalized.length}`;
            const messageInfo = readJsonRecord(row.message_data);
            const messageRole = readOptionalString(messageInfo?.role);
            if (messageInfo
                && messageRole === 'assistant'
                && messageInfo.error != null
                && !emittedMessageErrors.has(row.message_id)) {
                emittedMessageErrors.add(row.message_id);
                normalized.push(createNormalizedMessage({
                    id: `${baseId}_error`,
                    sessionId,
                    timestamp,
                    provider: PROVIDER,
                    kind: 'error',
                    content: formatToolContent(messageInfo.error),
                }));
            }
            if (!row.part_id) {
                continue;
            }
            const partData = readJsonRecord(row.part_data) ?? {};
            const partType = readOptionalString(partData.type);
            if (!partType) {
                continue;
            }
            if (partType === 'text') {
                const content = extractText(partData);
                if (content.trim()) {
                    normalized.push(createNormalizedMessage({
                        id: baseId,
                        sessionId,
                        timestamp,
                        provider: PROVIDER,
                        kind: 'text',
                        role: messageRole === 'user' ? 'user' : 'assistant',
                        content,
                    }));
                }
                continue;
            }
            if (partType === 'reasoning') {
                const content = extractText(partData);
                if (content.trim()) {
                    normalized.push(createNormalizedMessage({
                        id: baseId,
                        sessionId,
                        timestamp,
                        provider: PROVIDER,
                        kind: 'thinking',
                        content,
                    }));
                }
                continue;
            }
            if (partType === 'tool') {
                const state = readObjectRecord(partData.state) ?? {};
                const status = readOptionalString(state.status);
                const toolMessage = createNormalizedMessage({
                    id: baseId,
                    sessionId,
                    timestamp,
                    provider: PROVIDER,
                    kind: 'tool_use',
                    toolName: readOptionalString(partData.tool) ?? 'Tool',
                    toolInput: state.input ?? partData.input ?? {},
                    toolId: readOptionalString(partData.callID) ?? row.part_id,
                });
                if (status === 'completed' || status === 'error') {
                    toolMessage.toolResult = {
                        content: formatToolContent(state.output ?? state.error),
                        isError: status === 'error',
                    };
                }
                normalized.push(toolMessage);
                continue;
            }
            if (partType === 'step-finish') {
                normalized.push(createNormalizedMessage({
                    id: baseId,
                    sessionId,
                    timestamp,
                    provider: PROVIDER,
                    kind: 'stream_end',
                }));
                continue;
            }
            if (partType === 'patch' || partType === 'agent') {
                normalized.push(createNormalizedMessage({
                    id: baseId,
                    sessionId,
                    timestamp,
                    provider: PROVIDER,
                    kind: 'tool_use',
                    toolName: partType === 'patch' ? 'Patch' : 'Agent',
                    toolInput: partData,
                    toolId: row.part_id,
                }));
            }
        }
        return normalized;
    }
}
//# sourceMappingURL=opencode-sessions.provider.js.map