// Cost Analytics Engine (SPEC-182)
// Aggregations, trends, anomaly detection, and projections from token ledger data
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const TREND_WINDOW_DAYS = 7;
const TREND_STABLE_THRESHOLD = 10; // percent
const SPIKE_MULTIPLIER = 2;
const SPIKE_CRITICAL_MULTIPLIER = 3;
const SPIKE_WINDOW_DAYS = 14;
const HIGH_COST_TOOL_PERCENT = 0.5;
const DEFAULT_TOP_CONSUMERS_LIMIT = 10;
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function isoToDate(timestamp) {
    return timestamp.slice(0, 10); // YYYY-MM-DD
}
function modeOf(values) {
    if (values.length === 0) {
        return '';
    }
    const freq = new Map();
    for (const v of values) {
        freq.set(v, (freq.get(v) ?? 0) + 1);
    }
    let topKey = '';
    let topCount = 0;
    for (const [k, c] of freq) {
        if (c > topCount) {
            topCount = c;
            topKey = k;
        }
    }
    return topKey;
}
function average(nums) {
    if (nums.length === 0) {
        return 0;
    }
    return nums.reduce((s, n) => s + n, 0) / nums.length;
}
function daysInCalendarMonth(year, month) {
    return new Date(year, month, 0).getDate(); // month is 1-based here
}
// ---------------------------------------------------------------------------
// aggregateEntries
// ---------------------------------------------------------------------------
function computeEstimationAccuracy(estimatedCount, actualForEstimated, estimatedSum) {
    if (estimatedCount === 0 || actualForEstimated === 0) {
        return { estimationAccuracy: 100, overEstimationPercent: 0 };
    }
    return {
        estimationAccuracy: Math.min(100, (actualForEstimated / estimatedSum) * 100),
        overEstimationPercent: ((estimatedSum - actualForEstimated) / actualForEstimated) * 100,
    };
}
function buildEmptyAggregation() {
    return {
        totalTokens: 0,
        totalInputTokens: 0,
        totalOutputTokens: 0,
        totalCostUsd: 0,
        totalEstimatedCostUsd: 0,
        entryCount: 0,
        uniqueTools: 0,
        uniqueModels: 0,
        uniqueSessions: 0,
        avgTokensPerCall: 0,
        avgCostPerCall: 0,
        tokensByTool: {},
        costByTool: {},
        tokensByModel: {},
        costByModel: {},
        estimationAccuracy: 100,
        overEstimationPercent: 0,
    };
}
function incrementRecord(rec, key, val) {
    return { ...rec, [key]: (rec[key] ?? 0) + val };
}
export function aggregateEntries(entries) {
    if (entries.length === 0) {
        return buildEmptyAggregation();
    }
    const result = entries.reduce((a, e) => {
        const hasEst = e.estimatedTokens !== undefined;
        return {
            tT: a.tT + e.totalTokens,
            tI: a.tI + e.inputTokens,
            tO: a.tO + e.outputTokens,
            tC: a.tC + e.costUsd,
            tE: a.tE + (e.estimatedCostUsd ?? 0),
            eS: a.eS + (hasEst ? (e.estimatedTokens ?? 0) : 0),
            aE: a.aE + (hasEst ? e.totalTokens : 0),
            eC: a.eC + (hasEst ? 1 : 0),
            byTool: incrementRecord(a.byTool, e.toolName, e.totalTokens),
            cByTool: incrementRecord(a.cByTool, e.toolName, e.costUsd),
            byModel: incrementRecord(a.byModel, e.model, e.totalTokens),
            cByModel: incrementRecord(a.cByModel, e.model, e.costUsd),
            tools: new Set([...a.tools, e.toolName]),
            models: new Set([...a.models, e.model]),
            sessions: new Set([...a.sessions, e.sessionId]),
        };
    }, {
        tT: 0,
        tI: 0,
        tO: 0,
        tC: 0,
        tE: 0,
        eS: 0,
        aE: 0,
        eC: 0,
        byTool: {},
        cByTool: {},
        byModel: {},
        cByModel: {},
        tools: new Set(),
        models: new Set(),
        sessions: new Set(),
    });
    const { estimationAccuracy, overEstimationPercent } = computeEstimationAccuracy(result.eC, result.aE, result.eS);
    const count = entries.length;
    return {
        totalTokens: result.tT,
        totalInputTokens: result.tI,
        totalOutputTokens: result.tO,
        totalCostUsd: result.tC,
        totalEstimatedCostUsd: result.tE,
        entryCount: count,
        uniqueTools: result.tools.size,
        uniqueModels: result.models.size,
        uniqueSessions: result.sessions.size,
        avgTokensPerCall: result.tT / count,
        avgCostPerCall: result.tC / count,
        tokensByTool: result.byTool,
        costByTool: result.cByTool,
        tokensByModel: result.byModel,
        costByModel: result.cByModel,
        estimationAccuracy,
        overEstimationPercent,
    };
}
// ---------------------------------------------------------------------------
// computeDailyBreakdown
// ---------------------------------------------------------------------------
export function computeDailyBreakdown(entries) {
    const byDate = new Map();
    for (const e of entries) {
        const date = isoToDate(e.timestamp);
        const existing = byDate.get(date) ?? { tokens: 0, cost: 0, tools: [], models: [], count: 0 };
        existing.tokens += e.totalTokens;
        existing.cost += e.costUsd;
        existing.tools.push(e.toolName);
        existing.models.push(e.model);
        existing.count += 1;
        byDate.set(date, existing);
    }
    return Array.from(byDate.entries())
        .sort(([a], [b]) => a.localeCompare(b))
        .map(([date, data]) => ({
        date,
        totalTokens: data.tokens,
        totalCostUsd: data.cost,
        callCount: data.count,
        topTool: modeOf(data.tools),
        topModel: modeOf(data.models),
    }));
}
// ---------------------------------------------------------------------------
// detectTrend
// ---------------------------------------------------------------------------
export function detectTrend(dailyBreakdown) {
    if (dailyBreakdown.length < TREND_WINDOW_DAYS * 2) {
        return {
            direction: 'stable',
            changePercent: 0,
            description: 'Insufficient data to determine trend (need 14+ days).',
        };
    }
    const sorted = [...dailyBreakdown].sort((a, b) => a.date.localeCompare(b.date));
    const last14 = sorted.slice(-14);
    const prev7 = last14.slice(0, 7).map((d) => d.totalTokens);
    const curr7 = last14.slice(7).map((d) => d.totalTokens);
    const prevAvg = average(prev7);
    const currAvg = average(curr7);
    if (prevAvg === 0) {
        return { direction: 'stable', changePercent: 0, description: 'No previous data to compare.' };
    }
    const changePercent = ((currAvg - prevAvg) / prevAvg) * 100;
    let direction;
    if (changePercent > TREND_STABLE_THRESHOLD) {
        direction = 'increasing';
    }
    else if (changePercent < -TREND_STABLE_THRESHOLD) {
        direction = 'decreasing';
    }
    else {
        direction = 'stable';
    }
    const absPercent = Math.abs(changePercent).toFixed(0);
    const description = direction === 'stable'
        ? `Token usage is stable (${absPercent}% change week over week).`
        : `Token usage is ${direction} ${absPercent}% week over week.`;
    return { direction, changePercent: parseFloat(changePercent.toFixed(2)), description };
}
// ---------------------------------------------------------------------------
// detectAnomalies
// ---------------------------------------------------------------------------
export function detectAnomalies(entries, dailyBreakdown) {
    const anomalies = [];
    if (dailyBreakdown.length === 0) {
        return anomalies;
    }
    const sorted = [...dailyBreakdown].sort((a, b) => a.date.localeCompare(b.date));
    for (let i = 0; i < sorted.length; i++) {
        const day = sorted[i];
        if (day === undefined) {
            continue;
        }
        // --- Spike detection ---
        const windowStart = Math.max(0, i - SPIKE_WINDOW_DAYS);
        const windowDays = sorted.slice(windowStart, i);
        if (windowDays.length > 0) {
            const windowAvg = average(windowDays.map((d) => d.totalTokens));
            if (windowAvg > 0 && day.totalTokens > windowAvg * SPIKE_MULTIPLIER) {
                const severity = day.totalTokens > windowAvg * SPIKE_CRITICAL_MULTIPLIER ? 'critical' : 'warning';
                anomalies.push({
                    date: day.date,
                    type: 'spike',
                    severity,
                    description: `Token spike on ${day.date}: ${day.totalTokens.toLocaleString()} tokens vs ${Math.round(windowAvg).toLocaleString()} avg.`,
                    value: day.totalTokens,
                    threshold: windowAvg * SPIKE_MULTIPLIER,
                });
            }
        }
        // --- High cost tool detection ---
        const dayEntries = entries.filter((e) => isoToDate(e.timestamp) === day.date);
        if (day.totalCostUsd > 0 && dayEntries.length > 0) {
            const costByTool = new Map();
            for (const e of dayEntries) {
                costByTool.set(e.toolName, (costByTool.get(e.toolName) ?? 0) + e.costUsd);
            }
            for (const [tool, cost] of costByTool) {
                const fraction = cost / day.totalCostUsd;
                if (fraction > HIGH_COST_TOOL_PERCENT) {
                    anomalies.push({
                        date: day.date,
                        type: 'high_cost_tool',
                        severity: 'warning',
                        description: `Tool "${tool}" consumed ${(fraction * 100).toFixed(0)}% of daily cost on ${day.date}.`,
                        value: cost,
                        threshold: day.totalCostUsd * HIGH_COST_TOOL_PERCENT,
                    });
                }
            }
        }
    }
    return anomalies;
}
// ---------------------------------------------------------------------------
// computeBudgetStatus
// ---------------------------------------------------------------------------
export function computeBudgetStatus(entries, config) {
    const now = new Date();
    const year = now.getFullYear();
    const month = now.getMonth() + 1; // 1-based
    const daysInMonth = daysInCalendarMonth(year, month);
    const daysElapsed = now.getDate();
    // Current month filter
    const monthPrefix = `${year}-${String(month).padStart(2, '0')}`;
    const monthEntries = entries.filter((e) => e.timestamp.startsWith(monthPrefix));
    const spentUsd = monthEntries.reduce((sum, e) => sum + e.costUsd, 0);
    const dailyAvgUsd = daysElapsed > 0 ? spentUsd / daysElapsed : 0;
    const projectedMonthlyUsd = dailyAvgUsd * daysInMonth;
    const limit = config.monthlyLimitUsd ?? null;
    const remainingUsd = limit !== null ? Math.max(0, limit - spentUsd) : null;
    const percentUsed = limit !== null && limit > 0 ? (spentUsd / limit) * 100 : null;
    const warningFraction = config.warningThresholdPercent / 100;
    const criticalFraction = config.criticalThresholdPercent / 100;
    const isWarning = limit !== null && percentUsed !== null && percentUsed / 100 >= warningFraction;
    const isCritical = limit !== null && percentUsed !== null && percentUsed / 100 >= criticalFraction;
    const isOverBudget = limit !== null && spentUsd > limit;
    // Projected exhaustion day
    let projectedExhaustionDay = null;
    if (limit !== null && dailyAvgUsd > 0 && projectedMonthlyUsd > limit) {
        const daysUntilExhaustion = limit / dailyAvgUsd;
        projectedExhaustionDay = Math.min(daysInMonth, Math.ceil(daysUntilExhaustion));
    }
    return {
        monthlyLimitUsd: limit,
        spentUsd,
        remainingUsd,
        percentUsed: percentUsed !== null ? parseFloat(percentUsed.toFixed(2)) : null,
        projectedMonthlyUsd: parseFloat(projectedMonthlyUsd.toFixed(4)),
        projectedExhaustionDay,
        isWarning,
        isCritical,
        isOverBudget,
        daysInMonth,
        daysElapsed,
        dailyAvgUsd: parseFloat(dailyAvgUsd.toFixed(6)),
    };
}
// ---------------------------------------------------------------------------
// getTopConsumers
// ---------------------------------------------------------------------------
export function getTopConsumers(entries, limit = DEFAULT_TOP_CONSUMERS_LIMIT) {
    const totalTokens = entries.reduce((s, e) => s + e.totalTokens, 0);
    const byTool = new Map();
    for (const e of entries) {
        const existing = byTool.get(e.toolName) ?? { tokens: 0, cost: 0, count: 0 };
        existing.tokens += e.totalTokens;
        existing.cost += e.costUsd;
        existing.count += 1;
        byTool.set(e.toolName, existing);
    }
    return Array.from(byTool.entries())
        .map(([name, data]) => ({
        name,
        tokens: data.tokens,
        costUsd: data.cost,
        percentage: totalTokens > 0 ? parseFloat(((data.tokens / totalTokens) * 100).toFixed(2)) : 0,
        callCount: data.count,
    }))
        .sort((a, b) => b.tokens - a.tokens)
        .slice(0, limit);
}
// ---------------------------------------------------------------------------
// computeOptimizationSavings
// ---------------------------------------------------------------------------
export function computeOptimizationSavings(entries) {
    let tokensSavedByCache = 0;
    let tokensSavedByOptimization = 0;
    let totalCost = 0;
    for (const e of entries) {
        if (e.cached) {
            tokensSavedByCache += e.totalTokens;
        }
        if (e.optimizationSavings !== undefined && e.optimizationSavings > 0) {
            tokensSavedByOptimization += e.optimizationSavings;
        }
        totalCost += e.costUsd;
    }
    const totalTokensSaved = tokensSavedByCache + tokensSavedByOptimization;
    // Estimate cost saved: use average cost per token across all entries
    const totalActualTokens = entries.reduce((s, e) => s + e.totalTokens, 0);
    const avgCostPerToken = totalActualTokens > 0 ? totalCost / totalActualTokens : 0;
    const costSavedUsd = totalTokensSaved * avgCostPerToken;
    // savingsPercent relative to (total consumed + what was saved)
    const totalWithSavings = totalActualTokens + totalTokensSaved;
    const savingsPercent = totalWithSavings > 0 ? parseFloat(((totalTokensSaved / totalWithSavings) * 100).toFixed(2)) : 0;
    return {
        tokensSavedByCache,
        tokensSavedByOptimization,
        totalTokensSaved,
        costSavedUsd: parseFloat(costSavedUsd.toFixed(6)),
        savingsPercent,
    };
}
//# sourceMappingURL=analytics.js.map