import { setTimeout as sleep } from 'node:timers/promises';
import { z } from 'zod';
import { getCommand } from "../../../command-prefix.js";
import { openDatabase } from "../../../node-compat/sqlite.js";
import { isJSONMode, outputJSON } from "../../../output.js";
import * as tui from "../../../tui.js";
import { createSubcommand } from "../../../types.js";
import { REQUIRED_TABLES, isMemoryPath, getDefaultDBCandidates, resolveOpenCodeDBPath, } from "./db.js";
const DashboardOptionsSchema = z.object({
    json: z.boolean().optional().describe('Output JSON format'),
    session: z.string().optional().describe('Focus on a specific session ID'),
    watch: z.boolean().optional().describe('Continuously refresh the dashboard'),
    interval: z.number().optional().describe('Refresh interval in seconds (default: 5)'),
    limit: z.number().optional().describe('Maximum number of sessions to show (default: 10)'),
    since: z
        .string()
        .optional()
        .describe('Show sessions updated within duration (e.g., 1h, 6h, 24h, 7d)'),
    status: z.string().optional().describe('Filter by status: active, idle, error, archived'),
    all: z.boolean().optional().describe('Show all sessions (overrides default limit)'),
    search: z.string().optional().describe('Search sessions by title (fuzzy, case-insensitive)'),
    allTodos: z
        .boolean()
        .optional()
        .describe('Show all todos including completed (default: pending only)'),
});
function formatCost(cost) {
    if (!cost || cost.totalCost <= 0) {
        return '$0.00';
    }
    return `$${cost.totalCost.toFixed(2)}`;
}
const DURATION_UNITS = {
    m: 60 * 1000,
    h: 60 * 60 * 1000,
    d: 24 * 60 * 60 * 1000,
};
function parseDuration(duration) {
    const match = duration.match(/^(\d+)([mhd])$/);
    if (!match) {
        throw new Error(`Invalid duration format: "${duration}". Use a number followed by m (minutes), h (hours), or d (days). Examples: 30m, 1h, 7d`);
    }
    const value = parseInt(match[1], 10);
    const unit = match[2];
    const ms = DURATION_UNITS[unit];
    if (!ms) {
        throw new Error(`Unknown duration unit: "${unit}"`);
    }
    return Date.now() - value * ms;
}
function formatRelativeTime(epochMs) {
    const now = Date.now();
    const diffMs = now - epochMs;
    if (diffMs < 60 * 1000) {
        return 'just now';
    }
    if (diffMs < 60 * 60 * 1000) {
        const minutes = Math.floor(diffMs / (60 * 1000));
        return `${minutes}m ago`;
    }
    if (diffMs < 24 * 60 * 60 * 1000) {
        const hours = Math.floor(diffMs / (60 * 60 * 1000));
        return `${hours}h ago`;
    }
    if (diffMs < 7 * 24 * 60 * 60 * 1000) {
        const days = Math.floor(diffMs / (24 * 60 * 60 * 1000));
        return `${days}d ago`;
    }
    const date = new Date(epochMs);
    const yyyy = date.getFullYear();
    const mm = String(date.getMonth() + 1).padStart(2, '0');
    const dd = String(date.getDate()).padStart(2, '0');
    return `${yyyy}-${mm}-${dd}`;
}
function formatToolCount(activeTools) {
    const count = activeTools.length;
    if (count === 0)
        return '0';
    return `${count} run${count === 1 ? '' : 's'}`;
}
function computeStatus(session, latest, activeTools) {
    if (session.time_archived) {
        return { status: 'archived', lastActivity: session.time_updated };
    }
    if (session.time_compacting) {
        return { status: 'compacting', lastActivity: session.time_updated };
    }
    if (latest?.error) {
        return { status: 'error', lastActivity: latest.time_updated ?? session.time_updated };
    }
    const lastActivity = Math.max(session.time_updated, latest?.time_updated ?? 0);
    if (activeTools.length > 0) {
        return { status: 'active', lastActivity };
    }
    return { status: 'idle', lastActivity };
}
function buildSessionTree(sessions, messageCounts, activeTools, todos, costs, latestMessages) {
    const nodeMap = new Map();
    for (const session of sessions) {
        const sessionActiveTools = activeTools.get(session.id) ?? [];
        const sessionTodos = todos.get(session.id) ?? [];
        const sessionCost = costs.get(session.id) ?? { totalCost: 0, totalTokens: 0 };
        const latest = latestMessages.get(session.id);
        const { status, lastActivity } = computeStatus(session, latest, sessionActiveTools);
        nodeMap.set(session.id, {
            session,
            status,
            lastActivity,
            messageCount: messageCounts.get(session.id) ?? 0,
            activeTools: sessionActiveTools,
            todos: sessionTodos,
            cost: sessionCost,
            children: [],
        });
    }
    const roots = [];
    for (const node of nodeMap.values()) {
        const parentId = node.session.parent_id;
        if (parentId && nodeMap.has(parentId)) {
            const parent = nodeMap.get(parentId);
            if (parent) {
                parent.children.push(node);
            }
        }
        else {
            roots.push(node);
        }
    }
    const sortNodes = (nodes) => {
        nodes.sort((a, b) => b.session.time_updated - a.session.time_updated);
        nodes.forEach((node) => {
            sortNodes(node.children);
        });
    };
    sortNodes(roots);
    return { roots, allNodes: Array.from(nodeMap.values()) };
}
function flattenSessionRows(nodes) {
    const flattened = [];
    const walk = (node) => {
        flattened.push(node);
        for (const child of node.children) {
            walk(child);
        }
    };
    for (const node of nodes) {
        walk(node);
    }
    return flattened;
}
function filterTreeByStatus(nodes, targetStatus) {
    return nodes
        .map((node) => ({
        ...node,
        children: filterTreeByStatus(node.children, targetStatus),
    }))
        .filter((node) => node.status === targetStatus || node.children.length > 0);
}
async function loadDashboardData(dbPath, filterOpts) {
    const focusSessionId = filterOpts?.focusSessionId;
    const isMemory = isMemoryPath(dbPath);
    let db = null;
    try {
        db = await openDatabase(dbPath, isMemory ? undefined : { readonly: true });
        const tableRows = db
            .prepare("SELECT name FROM sqlite_master WHERE type = 'table'")
            .all();
        const foundTables = new Set(tableRows.map((row) => row.name));
        for (const table of REQUIRED_TABLES) {
            if (!foundTables.has(table)) {
                return {
                    error: 'schema',
                    message: 'OpenCode database schema is missing required tables.',
                };
            }
        }
        const sessions = db
            .prepare(`SELECT id, project_id, parent_id, title, time_created, time_updated, time_compacting, time_archived
				 FROM session
				 ORDER BY time_updated DESC`)
            .all();
        const messageCounts = new Map();
        const messageCountRows = db
            .prepare('SELECT session_id, COUNT(*) as count FROM message GROUP BY session_id')
            .all();
        for (const row of messageCountRows) {
            messageCounts.set(row.session_id, row.count);
        }
        const activeTools = new Map();
        const activeToolRows = db
            .prepare(`SELECT p.session_id as session_id,
						json_extract(p.data, '$.tool') as tool,
						json_extract(p.data, '$.state.status') as status,
						COALESCE(json_extract(p.data, '$.callID'), json_extract(p.data, '$.callId')) as call_id
				 FROM part p
				 WHERE json_valid(p.data)
					AND json_extract(p.data, '$.type') IN ('tool-invocation', 'tool')
					AND json_extract(p.data, '$.state.status') IN ('running', 'pending')`)
            .all();
        for (const row of activeToolRows) {
            const list = activeTools.get(row.session_id) ?? [];
            list.push({
                tool: row.tool ?? 'unknown',
                status: row.status ?? 'unknown',
                callId: row.call_id ?? null,
            });
            activeTools.set(row.session_id, list);
        }
        const todos = new Map();
        const todoRows = db
            .prepare('SELECT session_id, content, status, priority FROM todo ORDER BY session_id, position')
            .all();
        for (const row of todoRows) {
            const list = todos.get(row.session_id) ?? [];
            list.push({
                content: row.content,
                status: row.status,
                priority: row.priority ?? undefined,
            });
            todos.set(row.session_id, list);
        }
        const costs = new Map();
        const costRows = db
            .prepare(`SELECT session_id,
						SUM(json_extract(m.data, '$.cost')) as total_cost,
						SUM(
							COALESCE(json_extract(m.data, '$.tokens.input'), 0) +
							COALESCE(json_extract(m.data, '$.tokens.output'), 0)
						) as total_tokens
				 FROM message m
				 WHERE json_valid(m.data) AND json_extract(m.data, '$.cost') IS NOT NULL
				 GROUP BY session_id`)
            .all();
        for (const row of costRows) {
            costs.set(row.session_id, {
                totalCost: row.total_cost ?? 0,
                totalTokens: row.total_tokens ?? 0,
            });
        }
        const latestMessages = new Map();
        const latestRows = db
            .prepare(`SELECT m.session_id as session_id,
						CASE WHEN json_valid(m.data) THEN json_extract(m.data, '$.error') END as error,
						m.time_updated as time_updated
				 FROM message m
				 INNER JOIN (
					SELECT session_id, MAX(time_updated) as max_time
					FROM message
					GROUP BY session_id
				 ) latest
				 ON latest.session_id = m.session_id AND latest.max_time = m.time_updated`)
            .all();
        for (const row of latestRows) {
            latestMessages.set(row.session_id, row);
        }
        let filteredSessions = sessions;
        // Apply search filter at session level (case-insensitive LIKE)
        if (filterOpts?.search) {
            const query = filterOpts.search.toLowerCase();
            filteredSessions = filteredSessions.filter((s) => s.title.toLowerCase().includes(query));
        }
        const { roots, allNodes } = buildSessionTree(filteredSessions, messageCounts, activeTools, todos, costs, latestMessages);
        if (focusSessionId) {
            const focusNode = allNodes.find((node) => node.session.id === focusSessionId);
            if (!focusNode) {
                return {
                    error: 'not_found',
                    message: `Session ${focusSessionId} not found in database.`,
                };
            }
            return { data: { database: dbPath, sessions: [focusNode], allSessions: allNodes } };
        }
        // Apply post-build filters on root nodes
        let filteredRoots = roots;
        // Filter by --since (compare lastActivity against parsed timestamp)
        if (filterOpts?.since) {
            try {
                const sinceTimestamp = parseDuration(filterOpts.since);
                filteredRoots = filteredRoots.filter((node) => node.lastActivity >= sinceTimestamp);
            }
            catch {
                return {
                    error: 'invalid_duration',
                    message: `Invalid --since value: "${filterOpts.since}". Use format like 30m, 1h, 6h, 24h, 7d.`,
                };
            }
        }
        // Filter by --status (recursively prune non-matching nodes from tree)
        if (filterOpts?.status) {
            const targetStatus = filterOpts.status.toLowerCase();
            filteredRoots = filterTreeByStatus(filteredRoots, targetStatus);
        }
        // Apply --limit (default 10) unless --all is set
        if (!filterOpts?.all) {
            const limit = filterOpts?.limit ?? 10;
            filteredRoots = filteredRoots.slice(0, limit);
        }
        return { data: { database: dbPath, sessions: filteredRoots, allSessions: allNodes } };
    }
    catch (error) {
        return {
            error: 'query_failed',
            message: error instanceof Error ? error.message : 'Failed to query OpenCode database.',
        };
    }
    finally {
        if (db) {
            db.close();
        }
    }
}
function renderMissingDatabase(expectedPaths, envPath) {
    tui.newline();
    tui.output(`${tui.ICONS.info} OpenCode database not found.`);
    tui.newline();
    tui.output('The dashboard requires OpenCode v1.2.0+ with SQLite storage.');
    if (envPath) {
        tui.output(`Expected location: ${envPath}`);
    }
    else if (expectedPaths.length === 1) {
        tui.output(`Expected location: ${expectedPaths[0]}`);
    }
    else {
        tui.output('Expected locations:');
        for (const path of expectedPaths) {
            tui.output(`  ${tui.ICONS.bullet} ${path}`);
        }
    }
    tui.newline();
    tui.output('Tip: Set OPENCODE_DB_PATH to override the database location.');
}
function renderDashboard(data, watchMode, intervalSeconds, showAllTodos) {
    const flattened = flattenSessionRows(data.sessions);
    const activeCount = flattened.filter((node) => node.status === 'active').length;
    tui.newline();
    tui.output(tui.bold('Coder Dashboard'));
    tui.output('────────────────');
    tui.newline();
    tui.output(`Database: ${data.database}`);
    tui.newline();
    const sessionLabel = `Sessions (${activeCount} active)`;
    tui.output(tui.bold(sessionLabel));
    if (flattened.length === 0) {
        tui.info('No sessions found');
    }
    else {
        const rows = [];
        const walk = (node, prefix, isLast, isRoot) => {
            const connector = isRoot ? '' : isLast ? '└─ ' : '├─ ';
            rows.push({
                Session: `${prefix}${connector}${node.session.id}`,
                Status: node.status,
                'Last Active': formatRelativeTime(node.lastActivity),
                Messages: String(node.messageCount),
                Tools: formatToolCount(node.activeTools),
                Cost: formatCost(node.cost),
            });
            const childPrefix = isRoot ? '' : prefix + (isLast ? '   ' : '│  ');
            const lastIndex = node.children.length - 1;
            for (let i = 0; i < node.children.length; i++) {
                const child = node.children[i];
                if (!child)
                    continue;
                walk(child, childPrefix, i === lastIndex, false);
            }
        };
        const lastIndex = data.sessions.length - 1;
        for (let i = 0; i < data.sessions.length; i++) {
            const root = data.sessions[i];
            if (!root)
                continue;
            walk(root, '', i === lastIndex, true);
        }
        tui.table(rows, ['Session', 'Status', 'Last Active', 'Messages', 'Tools', 'Cost']);
    }
    const allTodoItems = [];
    for (const node of flattened) {
        for (const todo of node.todos) {
            allTodoItems.push({ sessionId: node.session.id, todo });
        }
    }
    const totalTodos = allTodoItems.length;
    const pendingTodos = allTodoItems.filter((item) => item.todo.status !== 'completed').length;
    const displayTodos = showAllTodos
        ? allTodoItems
        : allTodoItems.filter((item) => item.todo.status !== 'completed');
    tui.newline();
    const todosLabel = showAllTodos
        ? `Todos (${pendingTodos} pending / ${totalTodos} total)`
        : `Todos (${pendingTodos} pending)`;
    tui.output(tui.bold(todosLabel));
    if (displayTodos.length === 0 && totalTodos > 0) {
        tui.output(tui.muted(`${totalTodos} completed (use --all-todos to show)`));
    }
    else if (displayTodos.length === 0) {
        tui.output(tui.muted('No todos found'));
    }
    else {
        const showSessionLabel = new Set(displayTodos.map((item) => item.sessionId)).size > 1;
        for (const { sessionId, todo } of displayTodos) {
            const checked = todo.status === 'completed' ? 'x' : ' ';
            const sessionLabel = showSessionLabel ? `(${sessionId}) ` : '';
            tui.output(`- [${checked}] ${sessionLabel}${todo.content}`);
        }
    }
    if (watchMode) {
        tui.newline();
        const interval = intervalSeconds ?? 5;
        tui.output(tui.muted(`Press q to quit · r to refresh · refreshing every ${interval}s`));
    }
}
function serializeNode(node) {
    return {
        id: node.session.id,
        title: node.session.title,
        status: node.status,
        lastActivity: node.lastActivity,
        messageCount: node.messageCount,
        activeTools: node.activeTools,
        todos: node.todos,
        cost: node.cost,
        children: node.children.map(serializeNode),
    };
}
function clearTerminal() {
    if (!process.stdout.isTTY) {
        return;
    }
    process.stdout.write('\x1b[2J\x1b[H');
}
export const dashboardSubcommand = createSubcommand({
    name: 'dashboard',
    description: 'View Coder session dashboard',
    tags: ['read-only', 'fast'],
    schema: {
        options: DashboardOptionsSchema,
    },
    examples: [
        {
            command: getCommand('ai opencode dashboard'),
            description: 'Recent 10 sessions',
        },
        {
            command: getCommand('ai opencode dashboard --all'),
            description: 'All sessions',
        },
        {
            command: getCommand('ai opencode dashboard --since 1h'),
            description: 'Sessions active in last hour',
        },
        {
            command: getCommand('ai opencode dashboard --status active'),
            description: 'Only active sessions',
        },
        {
            command: getCommand('ai opencode dashboard --search "auth"'),
            description: 'Search by title',
        },
        {
            command: getCommand('ai opencode dashboard --limit 5 --status idle'),
            description: '5 most recent idle sessions',
        },
        {
            command: getCommand('ai opencode dashboard --watch --since 1h'),
            description: 'Watch recent sessions',
        },
        {
            command: getCommand('ai opencode dashboard --session ses_abc123'),
            description: 'Focus on a specific session tree',
        },
        {
            command: getCommand('ai opencode dashboard --json'),
            description: 'Output dashboard data as JSON',
        },
        {
            command: getCommand('ai opencode dashboard --all-todos'),
            description: 'Show all todos including completed',
        },
    ],
    async handler(ctx) {
        const { options, opts } = ctx;
        const jsonMode = isJSONMode(options) || opts?.json === true;
        const watchMode = opts?.watch === true;
        const intervalSeconds = opts?.interval ?? 5;
        const intervalMs = Math.max(1, intervalSeconds) * 1000;
        const showAllTodos = opts?.allTodos ?? false;
        const filterOpts = {
            focusSessionId: opts?.session,
            search: opts?.search,
            since: opts?.since,
            status: opts?.status,
            limit: opts?.limit,
            all: opts?.all,
        };
        const resolvedDbPath = await resolveOpenCodeDBPath();
        if (!resolvedDbPath) {
            const candidates = getDefaultDBCandidates();
            if (jsonMode) {
                outputJSON({
                    database: null,
                    sessions: [],
                    message: 'OpenCode database not found.',
                    expectedLocations: candidates,
                });
                return { success: false };
            }
            renderMissingDatabase(candidates, process.env.OPENCODE_DB_PATH);
            return { success: false };
        }
        const runOnce = async () => {
            const result = await loadDashboardData(resolvedDbPath, filterOpts);
            if (result.error || !result.data) {
                if (jsonMode) {
                    outputJSON({
                        database: resolvedDbPath,
                        sessions: [],
                        error: result.error,
                        message: result.message,
                    });
                    return false;
                }
                tui.newline();
                tui.error(result.message ?? 'Failed to load dashboard data.');
                return false;
            }
            if (jsonMode) {
                outputJSON({
                    database: result.data.database,
                    sessions: result.data.sessions.map(serializeNode),
                });
                return true;
            }
            renderDashboard(result.data, watchMode, intervalSeconds, showAllTodos);
            return true;
        };
        if (!watchMode || jsonMode) {
            const ok = await runOnce();
            return { success: ok };
        }
        // Set up keyboard input handling for watch mode
        if (process.stdin.isTTY) {
            process.stdin.setRawMode(true);
            process.stdin.resume();
            process.stdin.setEncoding('utf8');
        }
        let shouldExit = false;
        let shouldRefresh = false;
        process.stdin.on('data', (key) => {
            if (key === 'q' || key === '\x03') {
                // 'q' or Ctrl+C
                shouldExit = true;
            }
            if (key === 'r') {
                shouldRefresh = true;
            }
        });
        while (!shouldExit) {
            clearTerminal();
            await runOnce();
            // Sleep in small increments so we can respond to keypresses quickly
            const sleepChunk = 100;
            let elapsed = 0;
            while (elapsed < intervalMs && !shouldExit && !shouldRefresh) {
                await sleep(sleepChunk);
                elapsed += sleepChunk;
            }
            if (shouldRefresh) {
                shouldRefresh = false;
                // Loop continues immediately to refresh
            }
        }
        // Cleanup stdin state
        if (process.stdin.isTTY) {
            process.stdin.setRawMode(false);
            process.stdin.pause();
        }
        return { success: true };
    },
});
//# sourceMappingURL=dashboard.js.map