"use strict";
/**
 * Usage Aggregator Service
 *
 * Handles multi-instance usage data aggregation and caching.
 * Combines data from default Claude config and all CCS instances.
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.refreshUsageCache = exports.shutdownUsageAggregator = exports.prewarmUsageCache = exports.clearUsageCache = exports.getCachedHourlyData = exports.getCachedSessionData = exports.getCachedMonthlyData = exports.getCachedDailyData = exports.getUsageCacheSize = exports.getLastFetchTimestamp = exports.mergeSessionData = exports.mergeHourlyData = exports.mergeMonthlyData = exports.mergeDailyData = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const data_aggregator_1 = require("./data-aggregator");
const disk_cache_1 = require("./disk-cache");
const ui_1 = require("../../utils/ui");
const claude_config_path_1 = require("../../utils/claude-config-path");
const cliproxy_usage_syncer_1 = require("./cliproxy-usage-syncer");
const codex_native_usage_collector_1 = require("./codex-native-usage-collector");
const droid_native_usage_collector_1 = require("./droid-native-usage-collector");
const registry_cache_1 = require("../models-dev/registry-cache");
const model_identity_1 = require("./model-identity");
const profile_filter_1 = require("./profile-filter");
const config_loader_facade_1 = require("../../config/config-loader-facade");
const instance_directory_1 = require("../../management/instance-directory");
// ============================================================================
// Multi-Instance Support - Aggregate usage from CCS profiles
// ============================================================================
/** Path to CCS instances directory */
function getCcsInstancesDir() {
    return path.join((0, config_loader_facade_1.getCcsDir)(), 'instances');
}
function isPathWithinDir(childPath, parentPath) {
    const relative = path.relative(parentPath, childPath);
    return relative.length > 0 && !relative.startsWith('..') && !path.isAbsolute(relative);
}
function getDefaultProjectsDirForAnalytics() {
    const activeClaudeConfigDir = (0, claude_config_path_1.getClaudeConfigDir)();
    const instancesDir = getCcsInstancesDir();
    const claudeConfigDir = isPathWithinDir(activeClaudeConfigDir, instancesDir)
        ? (0, claude_config_path_1.getDefaultClaudeConfigDir)()
        : activeClaudeConfigDir;
    return path.join(claudeConfigDir, 'projects');
}
/**
 * Get list of CCS instance paths that have usage data
 * Only returns instances with existing projects/ directory
 */
function getInstancePaths() {
    const instancesDir = getCcsInstancesDir();
    if (!fs.existsSync(instancesDir)) {
        return [];
    }
    try {
        return (0, instance_directory_1.listAccountInstancePaths)(instancesDir).filter((instancePath) => {
            // Only include instances that have a projects directory
            const projectsPath = path.join(instancePath, 'projects');
            return fs.existsSync(projectsPath);
        });
    }
    catch {
        console.error((0, ui_1.fail)('Failed to read CCS instances directory'));
        return [];
    }
}
/**
 * Load usage data from a specific instance
 * Uses custom JSONL parser with instance's projects directory
 */
async function loadInstanceData(instancePath) {
    try {
        const projectsDir = path.join(instancePath, 'projects');
        const result = await (0, data_aggregator_1.loadAllUsageData)({ projectsDir });
        return result;
    }
    catch (_err) {
        // Instance may have no usage data - that's OK
        const instanceName = path.basename(instancePath);
        console.log((0, ui_1.info)(`No usage data in instance: ${instanceName}`));
        return { daily: [], hourly: [], monthly: [], session: [] };
    }
}
function getHourlyRequestCount(hour) {
    return hour.requestCount ?? hour.modelBreakdowns.length;
}
function finalizeDailyUsage(day) {
    const modelBreakdowns = (0, model_identity_1.coalesceLegacyProviderlessBreakdowns)(day.modelBreakdowns);
    return {
        ...day,
        modelsUsed: (0, model_identity_1.getModelsUsed)(modelBreakdowns),
        modelBreakdowns,
    };
}
function finalizeMonthlyUsage(month) {
    const modelBreakdowns = (0, model_identity_1.coalesceLegacyProviderlessBreakdowns)(month.modelBreakdowns);
    return {
        ...month,
        modelsUsed: (0, model_identity_1.getModelsUsed)(modelBreakdowns),
        modelBreakdowns,
    };
}
function finalizeHourlyUsage(hour) {
    const modelBreakdowns = (0, model_identity_1.coalesceLegacyProviderlessBreakdowns)(hour.modelBreakdowns);
    return {
        ...hour,
        modelsUsed: (0, model_identity_1.getModelsUsed)(modelBreakdowns),
        modelBreakdowns,
    };
}
/**
 * Merge daily usage data from multiple sources
 * Combines entries with same date by aggregating tokens
 */
function mergeDailyData(sources, options = {}) {
    const dateMap = new Map();
    for (const source of sources) {
        for (const day of source) {
            const mergeKey = options.preserveProfile ? `${day.profile ?? ''}\u0000${day.date}` : day.date;
            const existing = dateMap.get(mergeKey);
            if (existing) {
                // Aggregate tokens for same date
                existing.inputTokens += day.inputTokens;
                existing.outputTokens += day.outputTokens;
                existing.cacheCreationTokens += day.cacheCreationTokens;
                existing.cacheReadTokens += day.cacheReadTokens;
                existing.totalCost += day.totalCost;
                // Merge model breakdowns by aggregating same modelName
                for (const breakdown of day.modelBreakdowns) {
                    const breakdownKey = (0, model_identity_1.getProviderModelKey)(breakdown);
                    const existingBreakdown = existing.modelBreakdowns.find((b) => (0, model_identity_1.getProviderModelKey)(b) === breakdownKey);
                    if (existingBreakdown) {
                        existingBreakdown.inputTokens += breakdown.inputTokens;
                        existingBreakdown.outputTokens += breakdown.outputTokens;
                        existingBreakdown.cacheCreationTokens += breakdown.cacheCreationTokens;
                        existingBreakdown.cacheReadTokens += breakdown.cacheReadTokens;
                        existingBreakdown.cost += breakdown.cost;
                    }
                    else {
                        existing.modelBreakdowns.push({ ...breakdown });
                    }
                }
            }
            else {
                // Clone to avoid mutating original
                const modelBreakdowns = day.modelBreakdowns.map((b) => ({ ...b }));
                dateMap.set(mergeKey, {
                    ...day,
                    ...(options.preserveProfile && day.profile ? { profile: day.profile } : {}),
                    modelsUsed: (0, model_identity_1.getModelsUsed)(modelBreakdowns),
                    modelBreakdowns,
                });
            }
        }
    }
    return Array.from(dateMap.values())
        .map(finalizeDailyUsage)
        .sort((a, b) => a.date.localeCompare(b.date));
}
exports.mergeDailyData = mergeDailyData;
/**
 * Merge monthly usage data from multiple sources
 */
function mergeMonthlyData(sources, options = {}) {
    const monthMap = new Map();
    for (const source of sources) {
        for (const month of source) {
            const mergeKey = options.preserveProfile
                ? `${month.profile ?? ''}\u0000${month.month}`
                : month.month;
            const existing = monthMap.get(mergeKey);
            if (existing) {
                existing.inputTokens += month.inputTokens;
                existing.outputTokens += month.outputTokens;
                existing.cacheCreationTokens += month.cacheCreationTokens;
                existing.cacheReadTokens += month.cacheReadTokens;
                existing.totalCost += month.totalCost;
                for (const breakdown of month.modelBreakdowns) {
                    const breakdownKey = (0, model_identity_1.getProviderModelKey)(breakdown);
                    const existingBreakdown = existing.modelBreakdowns.find((item) => (0, model_identity_1.getProviderModelKey)(item) === breakdownKey);
                    if (existingBreakdown) {
                        existingBreakdown.inputTokens += breakdown.inputTokens;
                        existingBreakdown.outputTokens += breakdown.outputTokens;
                        existingBreakdown.cacheCreationTokens += breakdown.cacheCreationTokens;
                        existingBreakdown.cacheReadTokens += breakdown.cacheReadTokens;
                        existingBreakdown.cost += breakdown.cost;
                    }
                    else {
                        existing.modelBreakdowns.push({ ...breakdown });
                    }
                }
            }
            else {
                const modelBreakdowns = month.modelBreakdowns.map((breakdown) => ({ ...breakdown }));
                monthMap.set(mergeKey, {
                    ...month,
                    ...(options.preserveProfile && month.profile ? { profile: month.profile } : {}),
                    modelsUsed: (0, model_identity_1.getModelsUsed)(modelBreakdowns),
                    modelBreakdowns,
                });
            }
        }
    }
    return Array.from(monthMap.values())
        .map(finalizeMonthlyUsage)
        .sort((a, b) => a.month.localeCompare(b.month));
}
exports.mergeMonthlyData = mergeMonthlyData;
/**
 * Merge hourly usage data from multiple sources
 * Combines entries with same hour by aggregating tokens
 */
function mergeHourlyData(sources, options = {}) {
    const hourMap = new Map();
    for (const source of sources) {
        for (const hour of source) {
            const mergeKey = options.preserveProfile
                ? `${hour.profile ?? ''}\u0000${hour.hour}`
                : hour.hour;
            const existing = hourMap.get(mergeKey);
            if (existing) {
                existing.inputTokens += hour.inputTokens;
                existing.outputTokens += hour.outputTokens;
                existing.cacheCreationTokens += hour.cacheCreationTokens;
                existing.cacheReadTokens += hour.cacheReadTokens;
                existing.totalCost += hour.totalCost;
                existing.requestCount = getHourlyRequestCount(existing) + getHourlyRequestCount(hour);
                // Merge model breakdowns
                for (const breakdown of hour.modelBreakdowns) {
                    const breakdownKey = (0, model_identity_1.getProviderModelKey)(breakdown);
                    const existingBreakdown = existing.modelBreakdowns.find((b) => (0, model_identity_1.getProviderModelKey)(b) === breakdownKey);
                    if (existingBreakdown) {
                        existingBreakdown.inputTokens += breakdown.inputTokens;
                        existingBreakdown.outputTokens += breakdown.outputTokens;
                        existingBreakdown.cacheCreationTokens += breakdown.cacheCreationTokens;
                        existingBreakdown.cacheReadTokens += breakdown.cacheReadTokens;
                        existingBreakdown.cost += breakdown.cost;
                    }
                    else {
                        existing.modelBreakdowns.push({ ...breakdown });
                    }
                }
            }
            else {
                const modelBreakdowns = hour.modelBreakdowns.map((b) => ({ ...b }));
                hourMap.set(mergeKey, {
                    ...hour,
                    ...(options.preserveProfile && hour.profile ? { profile: hour.profile } : {}),
                    modelsUsed: (0, model_identity_1.getModelsUsed)(modelBreakdowns),
                    modelBreakdowns,
                    requestCount: getHourlyRequestCount(hour),
                });
            }
        }
    }
    return Array.from(hourMap.values())
        .map(finalizeHourlyUsage)
        .sort((a, b) => a.hour.localeCompare(b.hour));
}
exports.mergeHourlyData = mergeHourlyData;
/**
 * Merge session data from multiple sources
 * Deduplicates by sessionId (same session shouldn't appear in multiple instances)
 */
function mergeSessionData(sources) {
    const sessionMap = new Map();
    for (const source of sources) {
        for (const session of source) {
            // Use sessionId as unique key - later entries overwrite earlier ones
            sessionMap.set(session.sessionId, session);
        }
    }
    return Array.from(sessionMap.values()).sort((a, b) => new Date(b.lastActivity).getTime() - new Date(a.lastActivity).getTime());
}
exports.mergeSessionData = mergeSessionData;
// Cache TTLs (milliseconds)
const CACHE_TTL = {
    daily: 60 * 1000, // 1 minute - changes frequently
    monthly: 5 * 60 * 1000, // 5 minutes - aggregated data
    session: 60 * 1000, // 1 minute - user may refresh
};
/// Stale-while-revalidate: max age for stale data (7 days)
// We always show cached data to avoid blocking UI, refresh happens in background
const STALE_TTL = 7 * 24 * 60 * 60 * 1000;
// Track when data was last fetched (for UI indicator)
let lastFetchTimestamp = null;
/** Get timestamp of last successful data fetch */
function getLastFetchTimestamp() {
    return lastFetchTimestamp;
}
exports.getLastFetchTimestamp = getLastFetchTimestamp;
function getUsageCacheSize() {
    return cache.size;
}
exports.getUsageCacheSize = getUsageCacheSize;
// In-memory cache
const cache = new Map();
// Pending requests for coalescing (prevents duplicate concurrent calls)
const pendingRequests = new Map();
// Track if disk cache has been loaded into memory
let diskCacheInitialized = false;
// Track if background refresh is in progress
let isRefreshing = false;
// Coalesced full refresh promise shared across all usage loaders
let pendingFullRefresh = null;
/**
 * Persist cache to disk when we have enough data to be useful.
 */
function persistCacheIfComplete() {
    const daily = cache.get('daily');
    const hourly = cache.get('hourly');
    const monthly = cache.get('monthly');
    const session = cache.get('session');
    // Write if we have at least daily data (the most essential)
    if (daily) {
        (0, disk_cache_1.writeDiskCache)(daily.data, hourly?.data ?? [], monthly?.data ?? [], session?.data ?? []);
    }
}
/**
 * Load fresh data and update both memory and disk caches
 * Aggregates data from default ~/.claude/ AND all CCS instances
 */
async function refreshFromSource() {
    // Keep model metadata warming off the analytics request path. Current
    // refreshes use cached/static pricing; the background result helps future runs.
    void (0, registry_cache_1.startModelsDevRegistryRefresh)();
    // Try to sync CLIProxy snapshot before reading it.
    // Non-fatal: syncer handles unavailability and stale fallback.
    await (0, cliproxy_usage_syncer_1.syncCliproxyUsage)();
    // Load canonical default data and avoid counting the active instance twice
    const defaultData = (0, profile_filter_1.annotateUsageProfile)(await (0, data_aggregator_1.loadAllUsageData)({ projectsDir: getDefaultProjectsDirForAnalytics() }), 'default');
    // Load data from all CCS instances sequentially
    const instancePaths = getInstancePaths();
    const instanceDataResults = [];
    for (const instancePath of instancePaths) {
        try {
            const data = (0, profile_filter_1.annotateUsageProfile)(await loadInstanceData(instancePath), path.basename(instancePath));
            instanceDataResults.push(data);
        }
        catch (err) {
            const instanceName = path.basename(instancePath);
            console.error((0, ui_1.fail)(`Failed to load instance ${instanceName}: ${err}`));
        }
    }
    // Collect successful instance data
    const allDailySources = [defaultData.daily];
    const allHourlySources = [defaultData.hourly];
    const allMonthlySources = [defaultData.monthly];
    const allSessionSources = [defaultData.session];
    for (const result of instanceDataResults) {
        allDailySources.push(result.daily);
        allHourlySources.push(result.hourly);
        allMonthlySources.push(result.monthly);
        allSessionSources.push(result.session);
    }
    if (instanceDataResults.length > 0) {
        console.log((0, ui_1.info)(`Aggregated usage data from ${instanceDataResults.length} CCS instance(s)`));
    }
    try {
        const codexEntries = await (0, codex_native_usage_collector_1.scanCodexNativeUsageEntries)();
        if (codexEntries.length > 0) {
            allDailySources.push((0, data_aggregator_1.aggregateDailyUsage)(codexEntries, 'codex-native'));
            allHourlySources.push((0, data_aggregator_1.aggregateHourlyUsage)(codexEntries, 'codex-native'));
            allMonthlySources.push((0, data_aggregator_1.aggregateMonthlyUsage)(codexEntries, 'codex-native'));
            allSessionSources.push((0, data_aggregator_1.aggregateSessionUsage)(codexEntries, 'codex-native'));
            console.log((0, ui_1.info)(`Included native Codex usage data (${codexEntries.length} event(s))`));
        }
    }
    catch (err) {
        console.error((0, ui_1.fail)(`Failed to load native Codex usage data: ${err}`));
    }
    try {
        const droidEntries = await (0, droid_native_usage_collector_1.scanDroidNativeUsageEntries)();
        if (droidEntries.length > 0) {
            allDailySources.push((0, data_aggregator_1.aggregateDailyUsage)(droidEntries, 'droid-native'));
            allHourlySources.push((0, data_aggregator_1.aggregateHourlyUsage)(droidEntries, 'droid-native'));
            allMonthlySources.push((0, data_aggregator_1.aggregateMonthlyUsage)(droidEntries, 'droid-native'));
            allSessionSources.push((0, data_aggregator_1.aggregateSessionUsage)(droidEntries, 'droid-native'));
            console.log((0, ui_1.info)(`Included native Droid usage data (${droidEntries.length} event(s))`));
        }
    }
    catch (err) {
        console.error((0, ui_1.fail)(`Failed to load native Droid usage data: ${err}`));
    }
    // Load CLIProxy usage data (from local snapshot cache)
    try {
        const cliproxyData = await (0, cliproxy_usage_syncer_1.loadCachedCliproxyData)();
        if (cliproxyData.daily.length > 0) {
            allDailySources.push(cliproxyData.daily);
            allHourlySources.push(cliproxyData.hourly);
            allMonthlySources.push(cliproxyData.monthly);
            console.log((0, ui_1.info)('Included CLIProxy usage data'));
        }
    }
    catch (err) {
        console.error((0, ui_1.fail)(`Failed to load CLIProxy usage data: ${err}`));
    }
    // Merge all data sources
    const daily = mergeDailyData(allDailySources, { preserveProfile: true });
    const hourly = mergeHourlyData(allHourlySources, { preserveProfile: true });
    const monthly = mergeMonthlyData(allMonthlySources, { preserveProfile: true });
    const session = mergeSessionData(allSessionSources);
    // Update in-memory cache
    const now = Date.now();
    cache.set('daily', { data: daily, timestamp: now });
    cache.set('hourly', { data: hourly, timestamp: now });
    cache.set('monthly', { data: monthly, timestamp: now });
    cache.set('session', { data: session, timestamp: now });
    lastFetchTimestamp = now;
    // Persist to disk
    (0, disk_cache_1.writeDiskCache)(daily, hourly, monthly, session);
    return { daily, hourly, monthly, session };
}
async function refreshFromSourceCoalesced(force = false) {
    // Wait for any in-flight refresh to finish before starting a forced one
    // to prevent concurrent refreshes competing for the same resources
    if (force && pendingFullRefresh) {
        await pendingFullRefresh.catch(() => { });
    }
    if (force) {
        pendingFullRefresh = refreshFromSource().finally(() => {
            pendingFullRefresh = null;
        });
        return pendingFullRefresh;
    }
    if (pendingFullRefresh) {
        return pendingFullRefresh;
    }
    pendingFullRefresh = refreshFromSource().finally(() => {
        pendingFullRefresh = null;
    });
    return pendingFullRefresh;
}
/**
 * Initialize in-memory cache from disk cache (lazy - called on first API request).
 */
function ensureDiskCacheLoaded() {
    // Start sync when usage APIs are actually accessed.
    // startCliproxySync() is idempotent.
    (0, cliproxy_usage_syncer_1.startCliproxySync)();
    if (diskCacheInitialized)
        return;
    diskCacheInitialized = true;
    const diskCache = (0, disk_cache_1.readDiskCache)();
    if (!diskCache)
        return;
    // Load disk cache into memory (regardless of freshness)
    cache.set('daily', { data: diskCache.daily, timestamp: diskCache.timestamp });
    cache.set('hourly', { data: diskCache.hourly || [], timestamp: diskCache.timestamp });
    cache.set('monthly', { data: diskCache.monthly, timestamp: diskCache.timestamp });
    cache.set('session', { data: diskCache.session, timestamp: diskCache.timestamp });
    lastFetchTimestamp = diskCache.timestamp;
}
/**
 * Get cached data or fetch from loader with TTL
 * Implements stale-while-revalidate pattern for instant responses
 */
async function getCachedData(key, ttl, loader) {
    // Ensure disk cache is loaded on first request
    ensureDiskCacheLoaded();
    const cached = cache.get(key);
    const now = Date.now();
    // Fresh cache - return immediately
    if (cached && now - cached.timestamp < ttl) {
        return cached.data;
    }
    // Stale cache - return immediately, refresh in background (SWR pattern)
    if (cached && now - cached.timestamp < STALE_TTL) {
        // Fire and forget background refresh if not already pending
        if (!pendingRequests.has(key)) {
            const promise = loader()
                .then((data) => {
                cache.set(key, { data, timestamp: Date.now() });
                lastFetchTimestamp = Date.now();
                persistCacheIfComplete();
            })
                .catch((err) => {
                console.error((0, ui_1.fail)(`Background refresh failed for ${key}: ${err}`));
            })
                .finally(() => {
                pendingRequests.delete(key);
            });
            pendingRequests.set(key, promise);
        }
        return cached.data;
    }
    // No usable cache - check if request is already pending (coalesce)
    const pending = pendingRequests.get(key);
    if (pending) {
        return pending;
    }
    // Create new request
    const promise = loader()
        .then((data) => {
        cache.set(key, { data, timestamp: Date.now() });
        lastFetchTimestamp = Date.now();
        persistCacheIfComplete();
        return data;
    })
        .finally(() => {
        pendingRequests.delete(key);
    });
    pendingRequests.set(key, promise);
    return promise;
}
/** Cached loader for daily usage data */
async function getCachedDailyData(profile) {
    const data = await getCachedData('daily', CACHE_TTL.daily, async () => {
        return (await refreshFromSourceCoalesced()).daily;
    });
    return mergeDailyData([(0, profile_filter_1.filterByProfile)(data, profile)]);
}
exports.getCachedDailyData = getCachedDailyData;
/** Cached loader for monthly usage data */
async function getCachedMonthlyData(profile) {
    const data = await getCachedData('monthly', CACHE_TTL.monthly, async () => {
        return (await refreshFromSourceCoalesced()).monthly;
    });
    return mergeMonthlyData([(0, profile_filter_1.filterByProfile)(data, profile)]);
}
exports.getCachedMonthlyData = getCachedMonthlyData;
/** Cached loader for session data */
async function getCachedSessionData(profile) {
    const data = await getCachedData('session', CACHE_TTL.session, async () => {
        return (await refreshFromSourceCoalesced()).session;
    });
    return (0, profile_filter_1.filterByProfile)(data, profile);
}
exports.getCachedSessionData = getCachedSessionData;
/** Cached loader for hourly usage data */
async function getCachedHourlyData(profile) {
    const data = await getCachedData('hourly', CACHE_TTL.daily, async () => {
        return (await refreshFromSourceCoalesced()).hourly;
    });
    return mergeHourlyData([(0, profile_filter_1.filterByProfile)(data, profile)]);
}
exports.getCachedHourlyData = getCachedHourlyData;
/**
 * Clear all cached data (useful for manual refresh)
 */
function clearUsageCache() {
    cache.clear();
    pendingRequests.clear();
    pendingFullRefresh = null;
    (0, disk_cache_1.clearDiskCache)();
    // Reset so next API call will try to reload from disk/source
    diskCacheInitialized = false;
    lastFetchTimestamp = null;
}
exports.clearUsageCache = clearUsageCache;
/**
 * Pre-warm usage caches on server startup
 *
 * Strategy:
 * 1. Check disk cache - if fresh, use it (instant startup)
 * 2. If stale, use it immediately but trigger background refresh
 * 3. If no cache, return immediately and let first request trigger load
 */
async function prewarmUsageCache() {
    const start = Date.now();
    console.log((0, ui_1.info)('Pre-warming usage cache...'));
    try {
        // Start CLIProxy usage syncer early (runs in background every 5 min)
        (0, cliproxy_usage_syncer_1.startCliproxySync)();
        const diskCache = (0, disk_cache_1.readDiskCache)();
        // Fresh disk cache - use it directly
        if (diskCache && (0, disk_cache_1.isDiskCacheFresh)(diskCache)) {
            const now = Date.now();
            cache.set('daily', { data: diskCache.daily, timestamp: diskCache.timestamp });
            cache.set('hourly', { data: diskCache.hourly || [], timestamp: diskCache.timestamp });
            cache.set('monthly', { data: diskCache.monthly, timestamp: diskCache.timestamp });
            cache.set('session', { data: diskCache.session, timestamp: diskCache.timestamp });
            lastFetchTimestamp = diskCache.timestamp;
            const elapsed = Date.now() - start;
            console.log((0, ui_1.ok)(`Usage cache ready from disk (${elapsed}ms, cached ${(0, disk_cache_1.getCacheAge)(diskCache)})`));
            return { timestamp: now, elapsed, source: 'disk-fresh' };
        }
        // Stale disk cache - use it immediately, refresh in background
        if (diskCache && (0, disk_cache_1.isDiskCacheStale)(diskCache)) {
            const now = Date.now();
            cache.set('daily', { data: diskCache.daily, timestamp: diskCache.timestamp });
            cache.set('hourly', { data: diskCache.hourly || [], timestamp: diskCache.timestamp });
            cache.set('monthly', { data: diskCache.monthly, timestamp: diskCache.timestamp });
            cache.set('session', { data: diskCache.session, timestamp: diskCache.timestamp });
            lastFetchTimestamp = diskCache.timestamp;
            const elapsed = Date.now() - start;
            console.log((0, ui_1.ok)(`Usage cache ready from disk (${elapsed}ms, stale ${(0, disk_cache_1.getCacheAge)(diskCache)}, refreshing...)`));
            // Background refresh
            if (!isRefreshing) {
                isRefreshing = true;
                refreshFromSourceCoalesced()
                    .then(() => console.log((0, ui_1.ok)('Background refresh complete')))
                    .catch((err) => console.error((0, ui_1.fail)(`Background refresh failed: ${err}`)))
                    .finally(() => {
                    isRefreshing = false;
                });
            }
            return { timestamp: now, elapsed, source: 'disk-stale' };
        }
        // No usable disk cache - refresh from source (blocking for first startup only)
        console.log((0, ui_1.info)('No disk cache, loading from source...'));
        await refreshFromSourceCoalesced();
        const elapsed = Date.now() - start;
        console.log((0, ui_1.ok)(`Usage cache ready (${elapsed}ms)`));
        return { timestamp: Date.now(), elapsed, source: 'fresh' };
    }
    catch (err) {
        console.error((0, ui_1.fail)(`Failed to prewarm usage cache: ${err}`));
        throw err;
    }
}
exports.prewarmUsageCache = prewarmUsageCache;
/**
 * Shutdown usage aggregator cleanly (stops background syncer)
 */
function shutdownUsageAggregator() {
    (0, cliproxy_usage_syncer_1.stopCliproxySync)();
}
exports.shutdownUsageAggregator = shutdownUsageAggregator;
/**
 * Force refresh usage cache from all sources.
 * Used by manual refresh endpoint.
 */
async function refreshUsageCache() {
    // Ensure periodic sync is running for subsequent updates.
    (0, cliproxy_usage_syncer_1.startCliproxySync)();
    await refreshFromSourceCoalesced(true);
}
exports.refreshUsageCache = refreshUsageCache;
//# sourceMappingURL=aggregator.js.map