"use strict";
/**
 * Aggregate stats for the home page.
 *
 * Two data sources:
 *   - chats DB row counts (total, by status, today, week, top template,
 *     avg duration). Cheap — single sqlite roundtrip.
 *   - per-participant `_stats.json` files on disk under
 *     `~/.chorus/chats/<id>/round-<N>/<role-dir>/`. Walked at request
 *     time; for ~hundreds of chats this is sub-second. The on-disk file
 *     is the source of truth (per PR #16) — successful reviewer runs
 *     never write to phase_events, so DB-only would miss most cost.
 *
 * Endpoint stays additive — does not write anything, so we can cache it
 * later if call volume grows.
 */
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.registerStatsRoutes = registerStatsRoutes;
const fs_1 = __importDefault(require("fs"));
const os_1 = __importDefault(require("os"));
const path_1 = __importDefault(require("path"));
const index_js_1 = require("../../lib/db/index.js");
const voices_js_1 = require("../../lib/db/voices.js");
const api_response_js_1 = require("../api-response.js");
const DAY_MS = 24 * 60 * 60 * 1000;
function startOfTodayMs() {
    const now = new Date();
    return new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
}
function readStatsFile(filePath) {
    try {
        return JSON.parse(fs_1.default.readFileSync(filePath, 'utf-8'));
    }
    catch {
        return null;
    }
}
/**
 * Returns true if a participant directory name belongs to a shim that
 * charges the user out-of-pocket for each call (HTTP-dispatched API
 * shims). Kept as a pattern match on the shim's `name` (which forms the
 * middle slug of `reviewer-<name>-<idx>` / `doer-<name>`) so adding a new
 * paid shim is a one-line change here. Anything not matching is
 * considered "shadow" — subscription-tier CLI where the cost is sunk.
 */
const PAID_SHIMS = new Set(['openrouter']);
function isPaidParticipant(participantDirName) {
    // Strip the role prefix and trailing -<idx> for reviewers.
    // Doer dir: `doer-<shim>` → shim
    // Reviewer dir: `reviewer-<shim>-<idx>` → shim
    let core = participantDirName;
    if (core.startsWith('reviewer-'))
        core = core.slice('reviewer-'.length);
    else if (core.startsWith('doer-'))
        core = core.slice('doer-'.length);
    // Drop a trailing -<digit+>
    core = core.replace(/-\d+$/, '');
    return PAID_SHIMS.has(core);
}
/**
 * Walks every round/<role-dir>/_stats.json under a single chat dir and
 * sums up usage. Returns zeros if the dir doesn't exist or holds no
 * sidecars yet.
 */
function aggregateChatStats(chatDir) {
    const out = {
        costUsd: 0,
        actualCostUsd: 0,
        shadowCostUsd: 0,
        tokensIn: 0,
        tokensOut: 0,
    };
    if (!fs_1.default.existsSync(chatDir))
        return out;
    let rounds;
    try {
        rounds = fs_1.default.readdirSync(chatDir).filter((f) => f.startsWith('round-'));
    }
    catch {
        return out;
    }
    for (const round of rounds) {
        const roundDir = path_1.default.join(chatDir, round);
        let participantDirs;
        try {
            participantDirs = fs_1.default.readdirSync(roundDir);
        }
        catch {
            continue;
        }
        for (const pd of participantDirs) {
            const statsPath = path_1.default.join(roundDir, pd, '_stats.json');
            if (!fs_1.default.existsSync(statsPath))
                continue;
            const s = readStatsFile(statsPath);
            if (!s?.usage)
                continue;
            const cost = s.usage.costUsd ?? 0;
            out.costUsd += cost;
            if (isPaidParticipant(pd)) {
                out.actualCostUsd += cost;
            }
            else {
                out.shadowCostUsd += cost;
            }
            out.tokensIn += s.usage.inputTokens ?? 0;
            out.tokensOut += s.usage.outputTokens ?? 0;
        }
    }
    return out;
}
function registerStatsRoutes(fastify) {
    fastify.get('/stats', async () => {
        try {
            const allChats = await index_js_1.chats.list();
            const allVoices = await voices_js_1.voices.list({ enabled: true });
            const todayStart = startOfTodayMs();
            const weekStart = Date.now() - 7 * DAY_MS;
            const byStatus = {};
            const byTemplate = {};
            let runsToday = 0;
            let runsWeek = 0;
            let durationSum = 0;
            let durationCount = 0;
            let approvedCount = 0;
            let completedCount = 0;
            for (const c of allChats) {
                byStatus[c.status] = (byStatus[c.status] ?? 0) + 1;
                byTemplate[c.template_id] = (byTemplate[c.template_id] ?? 0) + 1;
                if (c.created_at >= todayStart)
                    runsToday++;
                if (c.created_at >= weekStart)
                    runsWeek++;
                if (c.finished_at) {
                    durationSum += c.finished_at - c.created_at;
                    durationCount++;
                }
                if (c.status !== 'drafting' && c.status !== 'reviewing')
                    completedCount++;
                if (c.status === 'approved' || c.status === 'merged')
                    approvedCount++;
            }
            const topEntry = Object.entries(byTemplate).sort((a, b) => b[1] - a[1])[0];
            const topTemplate = topEntry ? { id: topEntry[0], runs: topEntry[1] } : null;
            // Walk every chat's on-disk stats to get cost + tokens. Bounded
            // I/O — runs ~O(chats) and each chat has ≤6 sidecar files.
            const chatsRoot = path_1.default.join(os_1.default.homedir(), '.chorus', 'chats');
            let totalCostUsd = 0;
            let costTodayUsd = 0;
            let actualCostUsd = 0;
            let actualCostTodayUsd = 0;
            let shadowCostUsd = 0;
            let shadowCostTodayUsd = 0;
            let totalTokensIn = 0;
            let totalTokensOut = 0;
            for (const c of allChats) {
                const dir = path_1.default.join(chatsRoot, c.id);
                const s = aggregateChatStats(dir);
                totalCostUsd += s.costUsd;
                actualCostUsd += s.actualCostUsd;
                shadowCostUsd += s.shadowCostUsd;
                totalTokensIn += s.tokensIn;
                totalTokensOut += s.tokensOut;
                if (c.created_at >= todayStart) {
                    costTodayUsd += s.costUsd;
                    actualCostTodayUsd += s.actualCostUsd;
                    shadowCostTodayUsd += s.shadowCostUsd;
                }
            }
            const lineages = new Set();
            for (const v of allVoices)
                lineages.add(v.lineage);
            const summary = {
                totalRuns: allChats.length,
                runsToday,
                runsWeek,
                byStatus,
                approvalRate: completedCount > 0 ? approvedCount / completedCount : 0,
                avgDurationMs: durationCount > 0 ? Math.round(durationSum / durationCount) : 0,
                topTemplate,
                totalCostUsd,
                costTodayUsd,
                actualCostUsd,
                actualCostTodayUsd,
                shadowCostUsd,
                shadowCostTodayUsd,
                totalTokensIn,
                totalTokensOut,
                enabledVoices: allVoices.length,
                activeLineages: lineages.size,
            };
            return (0, api_response_js_1.successResponse)(summary);
        }
        catch (error) {
            const message = error instanceof Error ? error.message : 'Unknown error';
            return (0, api_response_js_1.errorResponse)('stats_error', message);
        }
    });
}
//# sourceMappingURL=stats.js.map