/**
 * Read-only service producing the rich overview dashboard projection.
 *
 * Aggregates spend, agent-slot usage, an active-runs list, and a
 * failure-by-reason breakdown from local SQLite run + runtime-event data.
 * Reuses the shared usage-cost helpers (single source of truth with the cap
 * monitor) and the run projection's redaction/tone helpers.
 */
import { listAllRuntimeEvents, sumRunUsageCostUsd, } from "../runs/runUsageCost.js";
import { readPublicTitle, toneForRunStatus, } from "../runs/runProjectionContract.js";
import { MetricsProjectionService } from "./metricsProjectionService.js";
import { OVERVIEW_ACTIVE_RUNS_LIMIT, OVERVIEW_DASHBOARD_PROJECTION_PUBLIC_SOURCE, OVERVIEW_FAILURE_REASON_CODES, OVERVIEW_FAILURE_REASON_LABELS, roundUsd, } from "./overviewDashboardProjectionContract.js";
/** Run statuses that currently occupy an agent slot (shown in the active list). */
const ACTIVE_RUN_STATUSES = new Set(["running", "queued"]);
/** Scan a generous window of runs so totals reflect recent history, not a page. */
const RUN_SCAN_LIMIT = 200;
export class OverviewDashboardProjectionService {
    store;
    constructor(store) {
        this.store = store;
    }
    buildOverviewDashboard(workspaceId) {
        const runs = this.store.listRunsForWorkspace(workspaceId, RUN_SCAN_LIMIT);
        const metrics = new MetricsProjectionService(this.store).buildMetrics(workspaceId);
        const perRunCostUsd = new Map();
        let spentUsdRaw = 0;
        let budgetUsdRaw = 0;
        let hasBudget = false;
        for (const run of runs) {
            const cost = sumRunUsageCostUsd(listAllRuntimeEvents(this.store, run.id));
            perRunCostUsd.set(run.id, cost);
            spentUsdRaw += cost;
            if (typeof run.capPolicy?.costUsdPerRun === "number") {
                budgetUsdRaw += run.capPolicy.costUsdPerRun;
                hasBudget = true;
            }
        }
        const spentUsd = roundUsd(spentUsdRaw);
        const budgetUsd = hasBudget ? roundUsd(budgetUsdRaw) : null;
        const budgetUtilization = budgetUsd && budgetUsd > 0
            ? Math.min(1, Math.round((spentUsd / budgetUsd) * 100) / 100)
            : null;
        const activeRunsAll = runs
            .filter((run) => ACTIVE_RUN_STATUSES.has(run.status))
            .sort((a, b) => compareUpdatedAtDesc(a, b));
        const activeRuns = activeRunsAll
            .slice(0, OVERVIEW_ACTIVE_RUNS_LIMIT)
            .map((run) => projectActiveRun(run, perRunCostUsd.get(run.id) ?? 0));
        const activeRunsTruncated = Math.max(0, activeRunsAll.length - activeRuns.length);
        const failuresByReason = buildFailuresByReason(runs);
        const policyBlocks = failuresByReason
            .filter((reason) => reason.reasonCode !== "failed")
            .reduce((sum, reason) => sum + reason.count, 0);
        const runningCount = runs.filter((run) => ACTIVE_RUN_STATUSES.has(run.status)).length;
        const reasonCode = runs.length === 0 ? "no-data" : "ok";
        return {
            contractVersion: "v1",
            generatedAt: new Date().toISOString(),
            source: OVERVIEW_DASHBOARD_PROJECTION_PUBLIC_SOURCE,
            workspaceId,
            reasonCode,
            stats: {
                runsRunning: metrics.counts.runsRunning,
                reviewQueueOpen: metrics.counts.reviewQueueOpen,
                runsCompleted: metrics.counts.runsCompleted,
                policyBlocks,
            },
            spend: { spentUsd, budgetUsd, budgetUtilization },
            agentSlots: {
                running: runningCount,
                // Not yet configurable on the bridge; UI shows "not configured".
                concurrencyCap: null,
            },
            activeRuns,
            activeRunsTruncated,
            failuresByReason,
        };
    }
}
function projectActiveRun(run, costUsd) {
    const status = run.status;
    return {
        id: run.id,
        title: readPublicTitle(run.title ?? null),
        status,
        tone: toneForRunStatus(status),
        costUsd: roundUsd(costUsd),
        updatedAt: run.updatedAt,
    };
}
function buildFailuresByReason(runs) {
    const counts = new Map(OVERVIEW_FAILURE_REASON_CODES.map((code) => [code, 0]));
    for (const run of runs) {
        if (counts.has(run.status)) {
            counts.set(run.status, (counts.get(run.status) ?? 0) + 1);
        }
    }
    // Fixed-enum order, zero-filled, so the UI shape is stable across calls.
    return OVERVIEW_FAILURE_REASON_CODES.map((reasonCode) => ({
        reasonCode,
        label: OVERVIEW_FAILURE_REASON_LABELS[reasonCode],
        count: counts.get(reasonCode) ?? 0,
    }));
}
function compareUpdatedAtDesc(a, b) {
    if (a.updatedAt === b.updatedAt)
        return a.id < b.id ? -1 : 1;
    return a.updatedAt < b.updatedAt ? 1 : -1;
}
