/**
 * Cost Tracker
 *
 * Tracks API costs during certifications with budget limits.
 *
 * @module cost/tracker
 */
import { randomUUID } from "crypto";
import { readFile, writeFile, mkdir } from "fs/promises";
import { join, dirname } from "path";
import { calculateCost, getModelProvider, formatCost, formatTokens } from "./pricing.js";
/**
 * Default budget configuration
 */
const DEFAULT_BUDGET = {
    maxCost: 10.0, // $10 default max
    warnAtPercent: 80,
    abortOnExceeded: true,
};
/**
 * Cost tracker for a certification
 */
export class CostTracker {
    state;
    options;
    persistPath;
    constructor(certificationId, projectPath, options) {
        this.options = {
            enabled: true,
            budget: DEFAULT_BUDGET,
            defaultModel: "claude-3.5-sonnet",
            logCosts: false,
            ...options,
        };
        this.state = {
            certificationId,
            projectPath,
            budget: { ...DEFAULT_BUDGET, ...options?.budget },
            calls: [],
            summary: this.createEmptySummary(),
            status: this.createInitialStatus(),
        };
        this.persistPath = join(projectPath, ".vaspera", "certifications", certificationId, "costs.json");
    }
    /**
     * Record an API call
     */
    recordCall(model, usage, options) {
        if (!this.options.enabled) {
            return this.createDummyRecord(model, usage);
        }
        const cost = calculateCost(model, usage);
        const record = {
            id: randomUUID().slice(0, 8),
            timestamp: new Date().toISOString(),
            model,
            provider: getModelProvider(model),
            usage,
            cost,
            agent: options?.agent,
            operation: options?.operation,
            cached: options?.cached,
        };
        this.state.calls.push(record);
        this.updateSummary(record);
        this.updateStatus();
        if (this.options.logCosts) {
            console.log(`[Cost] ${record.agent || "unknown"}: ${formatTokens(usage.totalTokens)} tokens, ${formatCost(cost.totalCost)}`);
        }
        return record;
    }
    /**
     * Check if within budget
     */
    isWithinBudget() {
        return this.state.status.withinBudget;
    }
    /**
     * Check budget and throw if exceeded and abortOnExceeded is true
     */
    checkBudget() {
        if (!this.isWithinBudget() && this.state.budget.abortOnExceeded) {
            const exceeded = this.state.status.exceeded.join(", ");
            throw new BudgetExceededError(`Budget exceeded: ${exceeded}`, this.state.summary, this.state.budget);
        }
    }
    /**
     * Get current summary
     */
    getSummary() {
        return { ...this.state.summary };
    }
    /**
     * Get current status
     */
    getStatus() {
        return { ...this.state.status };
    }
    /**
     * Get budget configuration
     */
    getBudget() {
        return { ...this.state.budget };
    }
    /**
     * Update budget configuration
     */
    setBudget(budget) {
        this.state.budget = { ...this.state.budget, ...budget };
        this.updateStatus();
    }
    /**
     * Get all call records
     */
    getCalls() {
        return [...this.state.calls];
    }
    /**
     * Get full state
     */
    getState() {
        return {
            ...this.state,
            calls: [...this.state.calls],
            summary: { ...this.state.summary },
            status: { ...this.state.status },
            budget: { ...this.state.budget },
        };
    }
    /**
     * Save state to file
     */
    async persist() {
        if (!this.persistPath)
            return;
        await mkdir(dirname(this.persistPath), { recursive: true });
        await writeFile(this.persistPath, JSON.stringify(this.state, null, 2));
    }
    /**
     * Load state from file
     */
    async load() {
        if (!this.persistPath)
            return false;
        try {
            const content = await readFile(this.persistPath, "utf-8");
            const loaded = JSON.parse(content);
            this.state = loaded;
            return true;
        }
        catch {
            return false;
        }
    }
    /**
     * Generate cost report
     */
    generateReport() {
        const { summary, budget, status } = this.state;
        const lines = [
            "# Cost Report",
            "",
            "## Summary",
            "",
            `- **Total Cost**: ${formatCost(summary.totalCost)}`,
            `- **Total Tokens**: ${formatTokens(summary.totalInputTokens + summary.totalOutputTokens)}`,
            `  - Input: ${formatTokens(summary.totalInputTokens)}`,
            `  - Output: ${formatTokens(summary.totalOutputTokens)}`,
            `  - Cached: ${formatTokens(summary.totalCachedTokens)}`,
            `- **API Calls**: ${summary.totalCalls}`,
            "",
        ];
        // Budget status
        if (budget.maxCost) {
            const usedPercent = (summary.totalCost / budget.maxCost) * 100;
            const bar = this.generateProgressBar(usedPercent);
            lines.push("## Budget", "", `- **Limit**: ${formatCost(budget.maxCost)}`, `- **Used**: ${formatCost(summary.totalCost)} (${usedPercent.toFixed(1)}%)`, `- ${bar}`, "");
        }
        // Status
        lines.push("## Status", "", `- **Within Budget**: ${status.withinBudget ? "Yes" : "No"}`);
        if (status.warnings.length > 0) {
            lines.push(`- **Warnings**: ${status.warnings.join(", ")}`);
        }
        if (status.exceeded.length > 0) {
            lines.push(`- **Exceeded**: ${status.exceeded.join(", ")}`);
        }
        // By model
        if (Object.keys(summary.byModel).length > 0) {
            lines.push("", "## By Model", "", "| Model | Calls | Tokens | Cost |", "|-------|-------|--------|------|");
            for (const [model, data] of Object.entries(summary.byModel)) {
                lines.push(`| ${model} | ${data.calls} | ${formatTokens(data.inputTokens + data.outputTokens)} | ${formatCost(data.cost)} |`);
            }
        }
        // By agent
        if (Object.keys(summary.byAgent).length > 0) {
            lines.push("", "## By Agent", "", "| Agent | Calls | Tokens | Cost |", "|-------|-------|--------|------|");
            for (const [agent, data] of Object.entries(summary.byAgent)) {
                lines.push(`| ${agent} | ${data.calls} | ${formatTokens(data.inputTokens + data.outputTokens)} | ${formatCost(data.cost)} |`);
            }
        }
        return lines.join("\n");
    }
    /**
     * Generate compact summary string
     */
    generateCompactSummary() {
        const { summary, status } = this.state;
        const statusIcon = status.withinBudget ? "✓" : "⚠";
        return `${statusIcon} Cost: ${formatCost(summary.totalCost)} | Tokens: ${formatTokens(summary.totalInputTokens + summary.totalOutputTokens)} | Calls: ${summary.totalCalls}`;
    }
    // Private methods
    createEmptySummary() {
        return {
            totalCost: 0,
            totalInputTokens: 0,
            totalOutputTokens: 0,
            totalCachedTokens: 0,
            totalCalls: 0,
            byModel: {},
            byAgent: {},
            startedAt: new Date().toISOString(),
            updatedAt: new Date().toISOString(),
        };
    }
    createInitialStatus() {
        return {
            withinBudget: true,
            costUsedPercent: 0,
            tokensUsedPercent: 0,
            callsUsedPercent: 0,
            warnings: [],
            exceeded: [],
        };
    }
    createDummyRecord(model, usage) {
        return {
            id: "disabled",
            timestamp: new Date().toISOString(),
            model,
            provider: getModelProvider(model),
            usage,
            cost: { inputCost: 0, outputCost: 0, cachedInputCost: 0, totalCost: 0 },
        };
    }
    updateSummary(record) {
        const s = this.state.summary;
        s.totalCost += record.cost.totalCost;
        s.totalInputTokens += record.usage.inputTokens;
        s.totalOutputTokens += record.usage.outputTokens;
        s.totalCachedTokens += record.usage.cachedInputTokens || 0;
        s.totalCalls += 1;
        s.updatedAt = record.timestamp;
        // By model
        if (!s.byModel[record.model]) {
            s.byModel[record.model] = { calls: 0, inputTokens: 0, outputTokens: 0, cost: 0 };
        }
        s.byModel[record.model].calls += 1;
        s.byModel[record.model].inputTokens += record.usage.inputTokens;
        s.byModel[record.model].outputTokens += record.usage.outputTokens;
        s.byModel[record.model].cost += record.cost.totalCost;
        // By agent
        const agent = record.agent || "unknown";
        if (!s.byAgent[agent]) {
            s.byAgent[agent] = { calls: 0, inputTokens: 0, outputTokens: 0, cost: 0 };
        }
        s.byAgent[agent].calls += 1;
        s.byAgent[agent].inputTokens += record.usage.inputTokens;
        s.byAgent[agent].outputTokens += record.usage.outputTokens;
        s.byAgent[agent].cost += record.cost.totalCost;
    }
    updateStatus() {
        const { summary, budget } = this.state;
        const status = {
            withinBudget: true,
            costUsedPercent: 0,
            tokensUsedPercent: 0,
            callsUsedPercent: 0,
            warnings: [],
            exceeded: [],
        };
        const warnPercent = budget.warnAtPercent || 80;
        // Check cost
        if (budget.maxCost) {
            status.costUsedPercent = (summary.totalCost / budget.maxCost) * 100;
            if (summary.totalCost > budget.maxCost) {
                status.withinBudget = false;
                status.exceeded.push(`cost (${formatCost(summary.totalCost)} > ${formatCost(budget.maxCost)})`);
            }
            else if (status.costUsedPercent >= warnPercent) {
                status.warnings.push(`cost at ${status.costUsedPercent.toFixed(1)}%`);
            }
        }
        // Check tokens
        const totalTokens = summary.totalInputTokens + summary.totalOutputTokens;
        if (budget.maxTotalTokens) {
            status.tokensUsedPercent = (totalTokens / budget.maxTotalTokens) * 100;
            if (totalTokens > budget.maxTotalTokens) {
                status.withinBudget = false;
                status.exceeded.push(`tokens (${formatTokens(totalTokens)} > ${formatTokens(budget.maxTotalTokens)})`);
            }
            else if (status.tokensUsedPercent >= warnPercent) {
                status.warnings.push(`tokens at ${status.tokensUsedPercent.toFixed(1)}%`);
            }
        }
        // Check calls
        if (budget.maxCalls) {
            status.callsUsedPercent = (summary.totalCalls / budget.maxCalls) * 100;
            if (summary.totalCalls > budget.maxCalls) {
                status.withinBudget = false;
                status.exceeded.push(`calls (${summary.totalCalls} > ${budget.maxCalls})`);
            }
            else if (status.callsUsedPercent >= warnPercent) {
                status.warnings.push(`calls at ${status.callsUsedPercent.toFixed(1)}%`);
            }
        }
        this.state.status = status;
    }
    generateProgressBar(percent) {
        const width = 20;
        const filled = Math.min(Math.floor((percent / 100) * width), width);
        const empty = width - filled;
        const bar = "█".repeat(filled) + "░".repeat(empty);
        return `[${bar}] ${percent.toFixed(1)}%`;
    }
}
/**
 * Error thrown when budget is exceeded
 */
export class BudgetExceededError extends Error {
    summary;
    budget;
    constructor(message, summary, budget) {
        super(message);
        this.summary = summary;
        this.budget = budget;
        this.name = "BudgetExceededError";
    }
}
/**
 * Global tracker registry
 */
const trackers = new Map();
/**
 * Get or create tracker for a certification
 */
export function getTracker(certificationId, projectPath, options) {
    const key = `${projectPath}:${certificationId}`;
    if (!trackers.has(key)) {
        trackers.set(key, new CostTracker(certificationId, projectPath, options));
    }
    return trackers.get(key);
}
/**
 * Remove tracker from registry
 */
export function removeTracker(certificationId, projectPath) {
    const key = `${projectPath}:${certificationId}`;
    trackers.delete(key);
}
/**
 * Get all active trackers
 */
export function getActiveTrackers() {
    return new Map(trackers);
}
// ---------------------------------------------------------------------------
// Cost Estimation & Forecasting
// ---------------------------------------------------------------------------
/**
 * Estimation factors based on typical certification patterns
 */
const ESTIMATION_FACTORS = {
    /** Tokens per file scanned (typical code file) */
    tokensPerFile: 1500,
    /** Tokens per finding analyzed */
    tokensPerFinding: 2000,
    /** Tokens for cross-verification per finding */
    tokensPerCrossVerify: 1000,
    /** Base overhead per agent */
    baseOverheadPerAgent: 3000,
    /** Output ratio (output/input) */
    outputRatio: 0.3,
    /** Safety margin multiplier */
    safetyMargin: 1.2,
};
/**
 * Estimate certification cost before running
 */
export function estimateCertificationCost(input) {
    const { fileCount, expectedFindings, agentCount = 3, crossVerify = true, model = "claude-3.5-sonnet", } = input;
    // Estimate findings if not provided (rough: 5% of files have findings)
    const estimatedFindings = expectedFindings ?? Math.ceil(fileCount * 0.05);
    // Calculate input tokens
    const scanningTokens = fileCount * ESTIMATION_FACTORS.tokensPerFile;
    const analysisTokens = estimatedFindings * ESTIMATION_FACTORS.tokensPerFinding;
    const crossVerifyTokens = crossVerify
        ? estimatedFindings * ESTIMATION_FACTORS.tokensPerCrossVerify
        : 0;
    const overheadTokens = agentCount * ESTIMATION_FACTORS.baseOverheadPerAgent;
    const totalInputTokens = Math.ceil((scanningTokens + analysisTokens + crossVerifyTokens + overheadTokens) *
        ESTIMATION_FACTORS.safetyMargin);
    const totalOutputTokens = Math.ceil(totalInputTokens * ESTIMATION_FACTORS.outputRatio);
    // Helper to create full TokenUsage
    const makeUsage = (input, output) => ({
        inputTokens: input,
        outputTokens: output,
        totalTokens: input + output,
    });
    // Calculate costs using pricing
    const cost = calculateCost(model, makeUsage(totalInputTokens, totalOutputTokens));
    // Determine confidence based on input quality
    let confidence = "medium";
    const assumptions = [];
    if (fileCount === 0) {
        confidence = "low";
        assumptions.push("No file count provided - using defaults");
    }
    if (!expectedFindings) {
        confidence = "low";
        assumptions.push("Finding count estimated at 5% of files");
    }
    if (expectedFindings && fileCount > 0) {
        confidence = "high";
    }
    assumptions.push(`Model: ${model}`);
    assumptions.push(`Agents: ${agentCount}`);
    assumptions.push(`Cross-verification: ${crossVerify ? "enabled" : "disabled"}`);
    assumptions.push(`Safety margin: ${((ESTIMATION_FACTORS.safetyMargin - 1) * 100).toFixed(0)}%`);
    // Calculate breakdown costs
    const scanInput = Math.ceil(scanningTokens * ESTIMATION_FACTORS.safetyMargin);
    const scanOutput = Math.ceil(scanInput * ESTIMATION_FACTORS.outputRatio);
    const analysisInput = Math.ceil(analysisTokens * ESTIMATION_FACTORS.safetyMargin);
    const analysisOutput = Math.ceil(analysisInput * ESTIMATION_FACTORS.outputRatio);
    const crossInput = Math.ceil(crossVerifyTokens * ESTIMATION_FACTORS.safetyMargin);
    const crossOutput = Math.ceil(crossInput * ESTIMATION_FACTORS.outputRatio);
    const overheadInput = Math.ceil(overheadTokens * ESTIMATION_FACTORS.safetyMargin);
    const overheadOutput = Math.ceil(overheadInput * ESTIMATION_FACTORS.outputRatio);
    return {
        estimatedCost: cost.totalCost,
        estimatedInputTokens: totalInputTokens,
        estimatedOutputTokens: totalOutputTokens,
        breakdown: {
            scanning: calculateCost(model, makeUsage(scanInput, scanOutput)).totalCost,
            analysis: calculateCost(model, makeUsage(analysisInput, analysisOutput)).totalCost,
            crossVerification: calculateCost(model, makeUsage(crossInput, crossOutput)).totalCost,
            overhead: calculateCost(model, makeUsage(overheadInput, overheadOutput)).totalCost,
        },
        confidence,
        model,
        assumptions,
    };
}
/**
 * Forecast future costs based on historical data
 */
export function forecastCost(historical, future) {
    if (historical.length === 0) {
        // No historical data, use default estimation
        return {
            ...estimateCertificationCost(future),
            historicalBasis: 0,
        };
    }
    // Calculate average cost per file from historical data
    const avgCostPerFile = historical.reduce((sum, h) => {
        return sum + (h.actualCost / Math.max(h.fileCount, 1));
    }, 0) / historical.length;
    // Calculate average tokens per file
    const avgInputPerFile = historical.reduce((sum, h) => {
        return sum + (h.actualInputTokens / Math.max(h.fileCount, 1));
    }, 0) / historical.length;
    const avgOutputPerFile = historical.reduce((sum, h) => {
        return sum + (h.actualOutputTokens / Math.max(h.fileCount, 1));
    }, 0) / historical.length;
    // Apply to future estimate
    const estimatedCost = avgCostPerFile * future.fileCount * ESTIMATION_FACTORS.safetyMargin;
    const estimatedInputTokens = Math.ceil(avgInputPerFile * future.fileCount * ESTIMATION_FACTORS.safetyMargin);
    const estimatedOutputTokens = Math.ceil(avgOutputPerFile * future.fileCount * ESTIMATION_FACTORS.safetyMargin);
    // More confidence with more historical data
    const confidence = historical.length >= 5 ? "high" :
        historical.length >= 2 ? "medium" : "low";
    return {
        estimatedCost,
        estimatedInputTokens,
        estimatedOutputTokens,
        breakdown: {
            scanning: estimatedCost * 0.4,
            analysis: estimatedCost * 0.35,
            crossVerification: estimatedCost * 0.15,
            overhead: estimatedCost * 0.1,
        },
        confidence,
        model: future.model || "claude-3.5-sonnet",
        assumptions: [
            `Based on ${historical.length} historical certification(s)`,
            `Average cost per file: ${formatCost(avgCostPerFile)}`,
            `Safety margin: ${((ESTIMATION_FACTORS.safetyMargin - 1) * 100).toFixed(0)}%`,
        ],
        historicalBasis: historical.length,
    };
}
/**
 * Format cost estimate as markdown
 */
export function formatCostEstimate(estimate) {
    return [
        "# Cost Estimate",
        "",
        `**Estimated Total**: ${formatCost(estimate.estimatedCost)}`,
        `**Confidence**: ${estimate.confidence}`,
        "",
        "## Token Estimate",
        "",
        `- Input: ${formatTokens(estimate.estimatedInputTokens)}`,
        `- Output: ${formatTokens(estimate.estimatedOutputTokens)}`,
        "",
        "## Cost Breakdown",
        "",
        `| Phase | Cost |`,
        `|-------|------|`,
        `| Scanning | ${formatCost(estimate.breakdown.scanning)} |`,
        `| Analysis | ${formatCost(estimate.breakdown.analysis)} |`,
        `| Cross-verification | ${formatCost(estimate.breakdown.crossVerification)} |`,
        `| Overhead | ${formatCost(estimate.breakdown.overhead)} |`,
        "",
        "## Assumptions",
        "",
        estimate.assumptions.map((a) => `- ${a}`).join("\n"),
    ].join("\n");
}
//# sourceMappingURL=tracker.js.map