/**
 * Token Usage Storage Service
 *
 * Tracks token usage per agent session with in-memory storage and
 * optional periodic flush to ~/.crewly/token-usage.json.
 *
 * @module services/monitoring/token-usage.service
 */
import { promises as fs } from 'fs';
import * as path from 'path';
/** File name for persisting token usage data */
const TOKEN_USAGE_FILE = 'token-usage.json';
/** Interval in milliseconds between periodic flushes to disk (5 minutes) */
const FLUSH_INTERVAL_MS = 300_000;
/**
 * Per-model token cost rates in USD per token.
 * Used for server-side cost calculation.
 */
export const TOKEN_COSTS = {
    'claude-3-opus': { input: 0.000015, output: 0.000075 },
    'claude-3-5-sonnet': { input: 0.000003, output: 0.000015 },
    'claude-3-haiku': { input: 0.00000025, output: 0.00000125 },
    'claude-opus-4-20250514': { input: 0.000015, output: 0.000075 },
    'claude-sonnet-4-20250514': { input: 0.000003, output: 0.000015 },
    'claude-haiku-4-20250506': { input: 0.00000025, output: 0.00000125 },
    'gemini-3-flash-preview': { input: 0.0000001, output: 0.0000004 },
    'gemini-2.5-flash-preview-05-20': { input: 0.00000015, output: 0.0000006 },
    'gemini-2.0-flash': { input: 0.0000001, output: 0.0000004 },
    'gpt-4o': { input: 0.000005, output: 0.000015 },
    'gpt-4o-mini': { input: 0.00000015, output: 0.0000006 },
    default: { input: 0.000003, output: 0.000015 },
};
/**
 * Calculate the cost for a given number of tokens and model.
 *
 * @param inputTokens - Number of input tokens (excluding cache tokens)
 * @param outputTokens - Number of output tokens
 * @param model - Model identifier
 * @returns Cost in USD
 */
export function calculateCost(inputTokens, outputTokens, model) {
    const rates = TOKEN_COSTS[model] || TOKEN_COSTS.default;
    return inputTokens * rates.input + outputTokens * rates.output;
}
/**
 * Stores a pre-calculated cost override for a session.
 * Used when the caller has more accurate cost data (e.g., cache-aware pricing
 * from Claude Code JSONL files).
 */
const sessionCostOverrides = new Map();
/**
 * Set a cost override for a session. When present, getSummary() uses this
 * instead of recalculating from events.
 */
export function setSessionCostOverride(sessionName, cost) {
    sessionCostOverrides.set(sessionName, cost);
}
/**
 * Get a cost override if one exists.
 */
export function getSessionCostOverride(sessionName) {
    return sessionCostOverrides.get(sessionName);
}
/**
 * Service for tracking and aggregating token usage across agent sessions.
 *
 * Uses an in-memory Map keyed by sessionName, with optional periodic
 * flush to ~/.crewly/token-usage.json for persistence across restarts.
 *
 * @example
 * ```typescript
 * const service = TokenUsageService.getInstance();
 * service.recordUsage('session-1', 'agent-a', 500, 200, 'claude-3-opus');
 * const summary = service.getUsageByAgent('agent-a');
 * ```
 */
export class TokenUsageService {
    static instance = null;
    /** In-memory storage keyed by session name */
    sessions = new Map();
    /** Active task context per session (sessionName → { taskId, runId }) */
    taskContexts = new Map();
    /** Timer handle for periodic flush */
    flushTimer = null;
    /** Directory for persisting usage data */
    storageDir;
    /**
     * Create a new TokenUsageService instance
     *
     * @param storageDir - Directory for persistence file (defaults to ~/.crewly)
     */
    constructor(storageDir) {
        this.storageDir = storageDir ?? path.join(process.env.HOME || '~', '.crewly');
    }
    /**
     * Get the singleton TokenUsageService instance
     *
     * @returns The shared TokenUsageService instance
     */
    static getInstance() {
        if (!TokenUsageService.instance) {
            TokenUsageService.instance = new TokenUsageService();
        }
        return TokenUsageService.instance;
    }
    /**
     * Reset the singleton instance (for testing)
     */
    static resetInstance() {
        if (TokenUsageService.instance) {
            TokenUsageService.instance.stopPeriodicFlush();
        }
        TokenUsageService.instance = null;
    }
    /**
     * Record a single token usage event for a session.
     *
     * Creates the session record if it doesn't exist, then appends the event
     * and updates running totals.
     *
     * @param sessionName - The session name (e.g. 'crewly-orc')
     * @param agentId - The agent identifier
     * @param input - Number of input tokens consumed
     * @param output - Number of output tokens generated
     * @param model - AI model identifier used
     */
    recordUsage(sessionName, agentId, input, output, model, taskId) {
        let record = this.sessions.get(sessionName);
        if (!record) {
            record = {
                sessionName,
                agentId,
                totalInput: 0,
                totalOutput: 0,
                eventCount: 0,
                events: [],
            };
            this.sessions.set(sessionName, record);
        }
        // Auto-apply task context if no explicit taskId provided
        const ctx = this.taskContexts.get(sessionName);
        const effectiveTaskId = taskId ?? ctx?.taskId ?? undefined;
        const event = {
            timestamp: new Date().toISOString(),
            agentId,
            input,
            output,
            model,
            taskId: effectiveTaskId,
        };
        record.events.push(event);
        record.totalInput += input;
        record.totalOutput += output;
        record.eventCount += 1;
    }
    /**
     * Set a cache-aware cost override for a session.
     * Used when the caller has pre-calculated cost with proper cache pricing.
     */
    overrideSessionCost(sessionName, cost) {
        setSessionCostOverride(sessionName, cost);
    }
    /**
     * Get aggregated token usage for a specific agent across all sessions.
     *
     * @param agentId - The agent identifier to query
     * @returns Aggregated usage summary for the agent
     */
    getUsageByAgent(agentId) {
        let totalInput = 0;
        let totalOutput = 0;
        let eventCount = 0;
        for (const record of this.sessions.values()) {
            if (record.agentId === agentId) {
                totalInput += record.totalInput;
                totalOutput += record.totalOutput;
                eventCount += record.eventCount;
            }
        }
        return { totalInput, totalOutput, eventCount };
    }
    /**
     * Get per-session usage summaries for the dashboard.
     *
     * @returns Array of session usage summaries
     */
    getUsageBySessions(taskId) {
        const summaries = [];
        for (const record of this.sessions.values()) {
            // Filter events by taskId if provided
            const events = taskId
                ? record.events.filter(e => e.taskId === taskId)
                : record.events;
            if (taskId && events.length === 0)
                continue;
            // Compute per-model breakdown
            const modelMap = new Map();
            let filteredInput = 0;
            let filteredOutput = 0;
            for (const event of events) {
                const existing = modelMap.get(event.model) || { input: 0, output: 0 };
                existing.input += event.input;
                existing.output += event.output;
                modelMap.set(event.model, existing);
                filteredInput += event.input;
                filteredOutput += event.output;
            }
            const modelBreakdown = [];
            for (const [model, usage] of modelMap) {
                modelBreakdown.push({
                    model,
                    inputTokens: usage.input,
                    outputTokens: usage.output,
                    cost: calculateCost(usage.input, usage.output, model),
                });
            }
            const calculatedCost = modelBreakdown.reduce((sum, m) => sum + m.cost, 0);
            // Use cache-aware cost override if available (from Claude JSONL sync)
            const costOverride = !taskId ? getSessionCostOverride(record.sessionName) : undefined;
            const totalCost = costOverride ?? calculatedCost;
            summaries.push({
                sessionName: record.sessionName,
                agentId: record.agentId,
                totalInput: taskId ? filteredInput : record.totalInput,
                totalOutput: taskId ? filteredOutput : record.totalOutput,
                eventCount: taskId ? events.length : record.eventCount,
                cost: totalCost,
                modelBreakdown,
            });
        }
        return summaries;
    }
    /**
     * Set the current task context for a session.
     * All subsequent recordUsage calls for this session will be tagged with the taskId.
     *
     * @param sessionName - Session name
     * @param taskId - Task ID to associate, or null to clear
     * @param runId - Optional run ID for precise run-level attribution
     */
    setTaskContext(sessionName, taskId, runId) {
        if (taskId === null) {
            this.taskContexts.set(sessionName, null);
        }
        else {
            this.taskContexts.set(sessionName, { taskId, runId });
        }
    }
    /**
     * Get the current task context for a session (taskId only, backward compatible).
     *
     * @param sessionName - Session name
     * @returns Current task ID or null
     */
    getTaskContext(sessionName) {
        const ctx = this.taskContexts.get(sessionName);
        return ctx?.taskId ?? null;
    }
    /**
     * Get the full task+run context for a session.
     *
     * @param sessionName - Session name
     * @returns Object with taskId and optional runId, or null if no context
     */
    getRunContext(sessionName) {
        return this.taskContexts.get(sessionName) ?? null;
    }
    /**
     * Get aggregated token usage grouped by task ID.
     *
     * @returns Map of taskId to aggregated token totals
     */
    getUsageByTask() {
        const taskMap = new Map();
        for (const record of this.sessions.values()) {
            for (const event of record.events) {
                const tid = event.taskId ?? 'unassigned';
                const existing = taskMap.get(tid) ?? { taskId: tid, totalInput: 0, totalOutput: 0, eventCount: 0, cost: 0 };
                existing.totalInput += event.input;
                existing.totalOutput += event.output;
                existing.eventCount += 1;
                existing.cost += calculateCost(event.input, event.output, event.model);
                taskMap.set(tid, existing);
            }
        }
        return taskMap;
    }
    /**
     * Get token usage events for a specific session recorded after a given timestamp.
     * Used to compute the token delta for a single Request when the orchestrator
     * handles the work directly (no WorkItem delegation).
     *
     * @param sessionName - The session to query (e.g. 'crewly-orc')
     * @param since - Only return events recorded at or after this time
     * @param until - Only return events before this time (defaults to now)
     * @returns Aggregated input tokens, output tokens, and cost in the window
     */
    getSessionUsageSince(sessionName, since, until) {
        const record = this.sessions.get(sessionName);
        if (!record)
            return { inputTokens: 0, outputTokens: 0, cost: 0 };
        const sinceMs = since.getTime();
        const untilMs = until ? until.getTime() : Infinity;
        let inputTokens = 0;
        let outputTokens = 0;
        let cost = 0;
        for (const event of record.events) {
            const eventMs = new Date(event.timestamp).getTime();
            if (eventMs >= sinceMs && eventMs <= untilMs) {
                inputTokens += event.input;
                outputTokens += event.output;
                cost += calculateCost(event.input, event.output, event.model);
            }
        }
        return { inputTokens, outputTokens, cost };
    }
    /**
     * Clear all tracked token usage data.
     */
    resetUsage() {
        this.sessions.clear();
    }
    /**
     * Start periodic flush of usage data to disk.
     *
     * Writes the current in-memory state to ~/.crewly/token-usage.json
     * at a fixed interval.
     */
    startPeriodicFlush() {
        if (this.flushTimer)
            return;
        this.flushTimer = setInterval(() => {
            this.flushToDisk().catch(() => {
                // Silently ignore flush errors — data is still in memory
            });
        }, FLUSH_INTERVAL_MS);
        // Allow Node to exit even if timer is running
        if (this.flushTimer.unref) {
            this.flushTimer.unref();
        }
    }
    /**
     * Stop the periodic flush timer.
     */
    stopPeriodicFlush() {
        if (this.flushTimer) {
            clearInterval(this.flushTimer);
            this.flushTimer = null;
        }
    }
    /**
     * Flush current usage data to disk.
     *
     * Writes sessions as a JSON array to the storage directory.
     */
    async flushToDisk() {
        const filePath = path.join(this.storageDir, TOKEN_USAGE_FILE);
        const data = Array.from(this.sessions.values());
        await fs.mkdir(this.storageDir, { recursive: true });
        await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8');
    }
    /**
     * Load previously persisted usage data from disk.
     *
     * Merges loaded data into the current in-memory state.
     */
    async loadFromDisk() {
        const filePath = path.join(this.storageDir, TOKEN_USAGE_FILE);
        try {
            const raw = await fs.readFile(filePath, 'utf-8');
            const records = JSON.parse(raw);
            for (const record of records) {
                if (!this.sessions.has(record.sessionName)) {
                    this.sessions.set(record.sessionName, record);
                }
            }
        }
        catch {
            // File doesn't exist or is invalid — start fresh
        }
    }
    /**
     * Get the number of tracked sessions (for testing).
     *
     * @returns Number of sessions in the store
     */
    getSessionCount() {
        return this.sessions.size;
    }
}
//# sourceMappingURL=token-usage.service.js.map