import { Database } from 'bun:sqlite';
import { existsSync } from 'node:fs';
import { homedir, platform } from 'node:os';
import { join } from 'node:path';
import { QUERIES } from './queries';
const REQUIRED_TABLES = new Set(['session', 'message', 'part', 'todo']);
const DEFAULT_LIMIT = 100;
const DEFAULT_TOOL_LIMIT = 50;
function safeParseJSON(value) {
    if (!value)
        return null;
    try {
        return JSON.parse(value);
    }
    catch {
        return null;
    }
}
function isMemoryPath(path) {
    return path === ':memory:' || path.includes('mode=memory');
}
function resolveDBPath(config) {
    if (config?.dbPath) {
        return config.dbPath;
    }
    const home = homedir();
    const candidates = [];
    const currentPlatform = platform();
    if (currentPlatform === 'darwin') {
        candidates.push(join(home, 'Library', 'Application Support', 'opencode', 'opencode.db'));
    }
    if (currentPlatform === 'win32') {
        const appData = process.env.APPDATA ?? join(home, 'AppData', 'Roaming');
        const localAppData = process.env.LOCALAPPDATA ?? join(home, 'AppData', 'Local');
        candidates.push(join(appData, 'opencode', 'opencode.db'));
        candidates.push(join(localAppData, 'opencode', 'opencode.db'));
    }
    // Linux default
    candidates.push(join(home, '.local', 'share', 'opencode', 'opencode.db'));
    for (const candidate of candidates) {
        if (existsSync(candidate)) {
            return candidate;
        }
    }
    return null;
}
function buildSessionSummary(row) {
    const diffs = safeParseJSON(row.summary_diffs);
    const hasSummary = row.summary_additions !== null ||
        row.summary_deletions !== null ||
        row.summary_files !== null ||
        diffs !== null;
    if (!hasSummary) {
        return undefined;
    }
    return {
        additions: row.summary_additions ?? undefined,
        deletions: row.summary_deletions ?? undefined,
        files: row.summary_files ?? undefined,
        diffs: diffs ?? undefined,
    };
}
function mapSession(row) {
    return {
        id: row.id,
        projectId: row.project_id,
        parentId: row.parent_id ?? undefined,
        slug: row.slug,
        directory: row.directory,
        title: row.title,
        version: row.version,
        shareUrl: row.share_url ?? undefined,
        summary: buildSessionSummary(row),
        timeCreated: row.time_created,
        timeUpdated: row.time_updated,
        timeCompacting: row.time_compacting ?? undefined,
        timeArchived: row.time_archived ?? undefined,
    };
}
function mapMessage(row) {
    const payload = safeParseJSON(row.data) ?? {};
    const tokens = payload.tokens;
    return {
        id: row.id,
        sessionId: row.session_id,
        role: typeof payload.role === 'string' ? payload.role : 'unknown',
        agent: typeof payload.agent === 'string' ? payload.agent : undefined,
        model: typeof payload.model === 'string' ? payload.model : undefined,
        cost: typeof payload.cost === 'number' ? payload.cost : undefined,
        tokens: tokens,
        error: typeof payload.error === 'string' ? payload.error : undefined,
        timeCreated: row.time_created,
        timeUpdated: row.time_updated,
    };
}
function mapToolCall(row) {
    const payload = safeParseJSON(row.data);
    if (!payload || (payload.type !== 'tool' && payload.type !== 'tool-invocation')) {
        return null;
    }
    const state = payload.state ?? {};
    const callId = payload.callID ?? payload.callId ?? '';
    return {
        id: row.id,
        messageId: row.message_id,
        sessionId: row.session_id,
        callId,
        tool: payload.tool ?? 'unknown',
        status: state.status ?? 'unknown',
        input: state.input,
        output: state.output,
        timeStarted: state.timeStarted ?? state.time_started,
        timeEnded: state.timeEnded ?? state.time_ended,
    };
}
function mapTextPart(row) {
    const payload = safeParseJSON(row.data);
    if (!payload || payload.type !== 'text' || typeof payload.text !== 'string') {
        return null;
    }
    return {
        id: row.id,
        messageId: row.message_id,
        sessionId: row.session_id,
        text: payload.text,
        timeCreated: row.time_created,
    };
}
function mapNonTextPart(row) {
    const payload = safeParseJSON(row.data);
    if (!payload || !payload.type || payload.type === 'text')
        return null;
    return {
        id: row.id,
        messageId: row.message_id,
        type: payload.type,
        toolName: payload.tool,
        timestamp: new Date(row.time_created).toISOString(),
    };
}
function mapToolCallSummary(row) {
    const payload = safeParseJSON(row.data);
    if (!payload || (payload.type !== 'tool' && payload.type !== 'tool-invocation'))
        return null;
    const state = payload.state ?? {};
    const inputStr = state.input !== undefined ? String(JSON.stringify(state.input)).slice(0, 200) : undefined;
    const outputStr = state.output !== undefined ? String(JSON.stringify(state.output)).slice(0, 200) : undefined;
    return {
        id: row.id,
        messageId: row.message_id,
        toolName: payload.tool ?? 'unknown',
        input: inputStr,
        output: outputStr,
        timestamp: new Date(row.time_created).toISOString(),
    };
}
function isNotNull(value) {
    return value !== null;
}
function summarizeTodos(todos) {
    const total = todos.length;
    const completed = todos.filter((todo) => todo.status === 'completed').length;
    const pending = total - completed;
    return { total, pending, completed };
}
function sumTreeCost(node) {
    return ((node.costSummary?.totalCost ?? 0) +
        node.children.reduce((sum, child) => sum + sumTreeCost(child), 0));
}
function createEmptySession(sessionId) {
    return {
        id: sessionId,
        projectId: 'unknown',
        parentId: undefined,
        slug: 'unknown',
        directory: '',
        title: 'Unknown Session',
        version: 'unknown',
        timeCreated: 0,
        timeUpdated: 0,
    };
}
export class OpenCodeDBReader {
    db = null;
    available = false;
    config;
    dbPath = null;
    statements = new Map();
    constructor(config) {
        this.config = {
            enableSchemaValidation: true,
            ...config,
        };
    }
    isAvailable() {
        if (this.available && this.db) {
            return true;
        }
        const resolved = resolveDBPath(this.config);
        if (!resolved)
            return false;
        if (isMemoryPath(resolved)) {
            return true;
        }
        return existsSync(resolved);
    }
    open() {
        if (this.db) {
            return this.available;
        }
        this.dbPath = resolveDBPath(this.config);
        if (!this.dbPath) {
            this.available = false;
            return false;
        }
        const isMemory = isMemoryPath(this.dbPath);
        if (this.config.dbPath && !isMemory && !existsSync(this.dbPath)) {
            this.available = false;
            return false;
        }
        try {
            if (isMemory) {
                // In-memory shared DBs (used in tests): open in readwrite mode
                // so pragmas can be set and the shared cache is accessible.
                this.db = new Database(this.dbPath);
                this.db.run('PRAGMA journal_mode = WAL');
            }
            else {
                // Real file-based DBs: open read-only for safety.
                // WAL is already configured by OpenCode; readers inherit it.
                this.db = new Database(this.dbPath, { readonly: true });
            }
            // busy_timeout is safe on both readonly and readwrite connections.
            this.db.run('PRAGMA busy_timeout = 3000');
        }
        catch (error) {
            console.warn('[OpenCodeDBReader] Failed to open database', error);
            this.available = false;
            this.db = null;
            return false;
        }
        if (this.config.enableSchemaValidation && !this.validateSchema()) {
            console.warn('[OpenCodeDBReader] Required tables missing in database');
            this.close();
            this.available = false;
            return false;
        }
        this.available = true;
        return true;
    }
    close() {
        if (this.db) {
            this.db.close();
        }
        this.db = null;
        this.available = false;
        this.statements.clear();
    }
    getSession(id) {
        if (!this.ensureOpen())
            return null;
        try {
            const statement = this.getStatement('GET_SESSION');
            const row = statement?.get(id);
            return row ? mapSession(row) : null;
        }
        catch (error) {
            console.warn('[OpenCodeDBReader] Failed to get session', error);
            return null;
        }
    }
    getChildSessions(parentId) {
        if (!this.ensureOpen())
            return [];
        try {
            const statement = this.getStatement('GET_CHILD_SESSIONS');
            const rows = statement?.all(parentId);
            return rows ? rows.map(mapSession) : [];
        }
        catch (error) {
            console.warn('[OpenCodeDBReader] Failed to get child sessions', error);
            return [];
        }
    }
    getSessionsByProject(projectId) {
        if (!this.ensureOpen())
            return [];
        try {
            const statement = this.getStatement('GET_SESSIONS_BY_PROJECT');
            const rows = statement?.all(projectId);
            return rows ? rows.map(mapSession) : [];
        }
        catch (error) {
            console.warn('[OpenCodeDBReader] Failed to get project sessions', error);
            return [];
        }
    }
    getSessionTree(rootId) {
        const visited = new Set();
        return this.buildSessionTree(rootId, visited);
    }
    getMessages(sessionId, opts) {
        if (!this.ensureOpen())
            return [];
        const limit = opts?.limit ?? DEFAULT_LIMIT;
        const offset = opts?.offset ?? 0;
        try {
            const statement = this.getStatement('GET_MESSAGES');
            const rows = statement?.all(sessionId, limit, offset);
            return rows ? rows.map(mapMessage) : [];
        }
        catch (error) {
            console.warn('[OpenCodeDBReader] Failed to get messages', error);
            return [];
        }
    }
    getLatestMessage(sessionId) {
        if (!this.ensureOpen())
            return null;
        try {
            const statement = this.getStatement('GET_LATEST_MESSAGE');
            const row = statement?.get(sessionId);
            return row ? mapMessage(row) : null;
        }
        catch (error) {
            console.warn('[OpenCodeDBReader] Failed to get latest message', error);
            return null;
        }
    }
    getMessageCount(sessionId) {
        if (!this.ensureOpen())
            return 0;
        try {
            const statement = this.getStatement('GET_MESSAGE_COUNT');
            const row = statement?.get(sessionId);
            return row?.count ?? 0;
        }
        catch (error) {
            console.warn('[OpenCodeDBReader] Failed to get message count', error);
            return 0;
        }
    }
    getActiveToolCalls(sessionId) {
        if (!this.ensureOpen())
            return [];
        try {
            const statement = this.getStatement('GET_ACTIVE_TOOLS');
            const rows = statement?.all(sessionId);
            return rows ? rows.map(mapToolCall).filter(isNotNull) : [];
        }
        catch (error) {
            console.warn('[OpenCodeDBReader] Failed to get active tools', error);
            return [];
        }
    }
    getToolCallHistory(sessionId, opts) {
        if (!this.ensureOpen())
            return [];
        const limit = opts?.limit ?? DEFAULT_TOOL_LIMIT;
        try {
            const statement = this.getStatement('GET_TOOL_HISTORY');
            const rows = statement?.all(sessionId, limit);
            return rows ? rows.map(mapToolCall).filter(isNotNull) : [];
        }
        catch (error) {
            console.warn('[OpenCodeDBReader] Failed to get tool history', error);
            return [];
        }
    }
    getTextParts(sessionId, opts) {
        if (!this.ensureOpen())
            return [];
        const limit = opts?.limit ?? DEFAULT_LIMIT;
        try {
            const statement = this.getStatement('GET_TEXT_PARTS');
            const rows = statement?.all(sessionId, limit);
            return rows ? rows.map(mapTextPart).filter(isNotNull) : [];
        }
        catch (error) {
            console.warn('[OpenCodeDBReader] Failed to get text parts', error);
            return [];
        }
    }
    /**
     * Get non-text parts (images, files, tool calls) for a session.
     * Useful for describing attachments during compaction.
     */
    getNonTextParts(sessionId) {
        if (!this.ensureOpen())
            return [];
        try {
            const statement = this.getStatement('GET_NON_TEXT_PARTS');
            const rows = statement?.all(sessionId, DEFAULT_LIMIT);
            return rows ? rows.map(mapNonTextPart).filter(isNotNull) : [];
        }
        catch (error) {
            console.warn('[OpenCodeDBReader] Failed to get non-text parts', error);
            return [];
        }
    }
    /**
     * Get recent tool calls for a session (newest first).
     * Returns concise summaries for compaction context.
     */
    getRecentToolCalls(sessionId, limit = 5) {
        if (!this.ensureOpen())
            return [];
        try {
            const statement = this.getStatement('GET_TOOL_HISTORY');
            const rows = statement?.all(sessionId, limit);
            return rows ? rows.map(mapToolCallSummary).filter(isNotNull) : [];
        }
        catch (error) {
            console.warn('[OpenCodeDBReader] Failed to get recent tool calls', error);
            return [];
        }
    }
    getTodos(sessionId) {
        if (!this.ensureOpen())
            return [];
        try {
            const statement = this.getStatement('GET_TODOS');
            const rows = statement?.all(sessionId);
            return rows
                ? rows.map((row) => ({
                    sessionId: row.session_id,
                    content: row.content,
                    status: row.status,
                    priority: row.priority,
                    position: row.position,
                }))
                : [];
        }
        catch (error) {
            console.warn('[OpenCodeDBReader] Failed to get todos', error);
            return [];
        }
    }
    getSessionCost(sessionId) {
        if (!this.ensureOpen()) {
            return {
                totalCost: 0,
                totalTokens: 0,
                inputTokens: 0,
                outputTokens: 0,
                reasoningTokens: 0,
                cacheRead: 0,
                cacheWrite: 0,
                messageCount: 0,
            };
        }
        try {
            const statement = this.getStatement('GET_SESSION_COST');
            const row = statement?.get(sessionId);
            return {
                totalCost: row?.total_cost ?? 0,
                totalTokens: row?.total_tokens ?? 0,
                inputTokens: row?.input_tokens ?? 0,
                outputTokens: row?.output_tokens ?? 0,
                reasoningTokens: row?.reasoning_tokens ?? 0,
                cacheRead: row?.cache_read ?? 0,
                cacheWrite: row?.cache_write ?? 0,
                messageCount: row?.message_count ?? 0,
            };
        }
        catch (error) {
            console.warn('[OpenCodeDBReader] Failed to get session cost', error);
            return {
                totalCost: 0,
                totalTokens: 0,
                inputTokens: 0,
                outputTokens: 0,
                reasoningTokens: 0,
                cacheRead: 0,
                cacheWrite: 0,
                messageCount: 0,
            };
        }
    }
    getSessionStatus(sessionId) {
        const session = this.getSession(sessionId);
        if (!session) {
            return { status: 'idle', lastActivity: 0 };
        }
        if (session.timeArchived) {
            return { status: 'archived', lastActivity: session.timeUpdated };
        }
        if (session.timeCompacting) {
            return { status: 'compacting', lastActivity: session.timeUpdated };
        }
        const latest = this.getLatestMessage(sessionId);
        if (latest?.error) {
            return { status: 'error', lastActivity: latest.timeUpdated };
        }
        const activeTools = this.getActiveToolCalls(sessionId);
        const lastActivity = Math.max(session.timeUpdated, latest?.timeUpdated ?? 0);
        if (activeTools.length > 0) {
            return { status: 'active', lastActivity };
        }
        return { status: 'idle', lastActivity };
    }
    searchSessions(query, opts) {
        if (!this.ensureOpen())
            return [];
        try {
            const pattern = `%${query}%`;
            if (opts?.limit !== undefined) {
                const statement = this.getStatement('SEARCH_SESSIONS_LIMITED');
                const rows = statement?.all(pattern, opts.limit);
                return rows ? rows.map(mapSession) : [];
            }
            const statement = this.getStatement('SEARCH_SESSIONS');
            const rows = statement?.all(pattern);
            return rows ? rows.map(mapSession) : [];
        }
        catch (error) {
            console.warn('[OpenCodeDBReader] Failed to search sessions', error);
            return [];
        }
    }
    getSessionDashboard(parentSessionId) {
        const sessions = this.getChildSessions(parentSessionId).map((child) => this.getSessionTree(child.id));
        const totalCost = sessions.reduce((sum, node) => sum + sumTreeCost(node), 0);
        return { sessions, totalCost };
    }
    ensureOpen() {
        if (this.db && this.available) {
            return true;
        }
        return this.open();
    }
    getStatement(key) {
        if (!this.db)
            return null;
        const existing = this.statements.get(key);
        if (existing)
            return existing;
        const statement = this.db.prepare(QUERIES[key]);
        this.statements.set(key, statement);
        return statement;
    }
    validateSchema() {
        if (!this.db)
            return false;
        try {
            const statement = this.db.prepare(QUERIES.CHECK_TABLES);
            const rows = statement.all();
            const found = new Set(rows.map((row) => row.name));
            for (const table of REQUIRED_TABLES) {
                if (!found.has(table)) {
                    return false;
                }
            }
            return true;
        }
        catch (error) {
            console.warn('[OpenCodeDBReader] Failed to validate schema', error);
            return false;
        }
    }
    buildSessionTree(rootId, visited) {
        if (visited.has(rootId)) {
            console.warn('[OpenCodeDBReader] Detected session cycle', rootId);
            return {
                session: createEmptySession(rootId),
                children: [],
                messageCount: 0,
                activeToolCount: 0,
            };
        }
        visited.add(rootId);
        const session = this.getSession(rootId) ?? createEmptySession(rootId);
        const children = this.getChildSessions(rootId).map((child) => this.buildSessionTree(child.id, visited));
        const messageCount = this.getMessageCount(rootId);
        const activeToolCount = this.getActiveToolCalls(rootId).length;
        const todos = this.getTodos(rootId);
        const costSummary = this.getSessionCost(rootId);
        return {
            session,
            children,
            messageCount,
            activeToolCount,
            todoSummary: todos.length > 0 ? summarizeTodos(todos) : undefined,
            costSummary,
        };
    }
}
//# sourceMappingURL=reader.js.map