// storage/token-ledger-store.ts — Token usage ledger persistence (SPEC-182)
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { withFileLock } from './file-mutex.js';
const MAX_FILE_SIZE_MB = 10;
const ROTATION_MONTHS = 6;
// ---------------------------------------------------------------------------
// Path helpers
// ---------------------------------------------------------------------------
function getLedgerDir(projectHash, dataDir) {
    return join(dataDir, 'projects', projectHash, 'token-ledger');
}
function getLedgerPath(projectHash, dataDir, month) {
    return join(getLedgerDir(projectHash, dataDir), `${month}.json`);
}
function getSummaryPath(projectHash, dataDir) {
    return join(getLedgerDir(projectHash, dataDir), 'summary.json');
}
function currentMonth() {
    return new Date().toISOString().slice(0, 7); // YYYY-MM
}
function ensureDir(dirPath) {
    if (!existsSync(dirPath)) {
        mkdirSync(dirPath, { recursive: true });
    }
}
// ---------------------------------------------------------------------------
// Date range helpers
// ---------------------------------------------------------------------------
function getStartOfToday() {
    const d = new Date();
    d.setHours(0, 0, 0, 0);
    return d.toISOString();
}
function getStartOfWeek() {
    const d = new Date();
    d.setDate(d.getDate() - 6);
    d.setHours(0, 0, 0, 0);
    return d.toISOString();
}
function getStartOfMonth() {
    const d = new Date();
    d.setDate(1);
    d.setHours(0, 0, 0, 0);
    return d.toISOString();
}
function filterByPeriod(entries, period) {
    if (!period || period === 'all_time') {
        return entries;
    }
    let cutoff;
    if (period === 'today') {
        cutoff = getStartOfToday();
    }
    else if (period === 'week') {
        cutoff = getStartOfWeek();
    }
    else {
        cutoff = getStartOfMonth();
    }
    return entries.filter((e) => e.timestamp >= cutoff);
}
// ---------------------------------------------------------------------------
// Low-level file I/O
// ---------------------------------------------------------------------------
function readMonthFile(filePath) {
    if (!existsSync(filePath)) {
        return [];
    }
    try {
        const raw = readFileSync(filePath, 'utf-8');
        const parsed = JSON.parse(raw);
        if (!Array.isArray(parsed)) {
            return [];
        }
        return parsed;
    }
    catch {
        return [];
    }
}
function writeMonthFile(filePath, entries) {
    ensureDir(dirname(filePath));
    const json = JSON.stringify(entries, null, 2);
    const sizeMb = Buffer.byteLength(json, 'utf-8') / (1024 * 1024);
    if (sizeMb > MAX_FILE_SIZE_MB) {
        // Keep newest entries, drop oldest
        const sorted = [...entries].sort((a, b) => b.timestamp.localeCompare(a.timestamp));
        const trimmed = trimToMaxSize(sorted);
        writeFileSync(filePath, JSON.stringify(trimmed, null, 2), 'utf-8');
        return;
    }
    writeFileSync(filePath, json, 'utf-8');
}
function trimToMaxSize(sorted) {
    const maxBytes = MAX_FILE_SIZE_MB * 1024 * 1024;
    const trimmed = [];
    let totalSize = 0;
    for (const entry of sorted) {
        const entrySize = Buffer.byteLength(JSON.stringify(entry), 'utf-8');
        if (totalSize + entrySize > maxBytes) {
            break;
        }
        trimmed.push(entry);
        totalSize += entrySize;
    }
    return trimmed;
}
// ---------------------------------------------------------------------------
// Aggregation computation
// ---------------------------------------------------------------------------
function computeAggregation(entries) {
    if (entries.length === 0) {
        return emptyAggregation();
    }
    const tokensByTool = {};
    const costByTool = {};
    const tokensByModel = {};
    const costByModel = {};
    const tools = new Set();
    const models = new Set();
    const sessions = new Set();
    let totalTokens = 0;
    let totalInputTokens = 0;
    let totalOutputTokens = 0;
    let totalCostUsd = 0;
    let totalEstimatedCostUsd = 0;
    let totalActualForAccuracy = 0;
    let totalEstimatedForAccuracy = 0;
    let estimationCount = 0;
    for (const e of entries) {
        totalTokens += e.totalTokens;
        totalInputTokens += e.inputTokens;
        totalOutputTokens += e.outputTokens;
        totalCostUsd += e.costUsd;
        if (e.estimatedCostUsd !== undefined) {
            totalEstimatedCostUsd += e.estimatedCostUsd;
        }
        if (e.estimatedTokens !== undefined) {
            totalActualForAccuracy += e.totalTokens;
            totalEstimatedForAccuracy += e.estimatedTokens;
            estimationCount++;
        }
        tools.add(e.toolName);
        models.add(e.model);
        sessions.add(e.sessionId);
        tokensByTool[e.toolName] = (tokensByTool[e.toolName] ?? 0) + e.totalTokens;
        costByTool[e.toolName] = (costByTool[e.toolName] ?? 0) + e.costUsd;
        tokensByModel[e.model] = (tokensByModel[e.model] ?? 0) + e.totalTokens;
        costByModel[e.model] = (costByModel[e.model] ?? 0) + e.costUsd;
    }
    const count = entries.length;
    const estimationAccuracy = computeEstimationAccuracy(totalActualForAccuracy, totalEstimatedForAccuracy, estimationCount);
    const overEstimationPercent = computeOverEstimationPercent(totalActualForAccuracy, totalEstimatedForAccuracy, estimationCount);
    return {
        totalTokens,
        totalInputTokens,
        totalOutputTokens,
        totalCostUsd,
        totalEstimatedCostUsd,
        entryCount: count,
        uniqueTools: tools.size,
        uniqueModels: models.size,
        uniqueSessions: sessions.size,
        avgTokensPerCall: count > 0 ? totalTokens / count : 0,
        avgCostPerCall: count > 0 ? totalCostUsd / count : 0,
        tokensByTool,
        costByTool,
        tokensByModel,
        costByModel,
        estimationAccuracy,
        overEstimationPercent,
    };
}
function computeEstimationAccuracy(actual, estimated, count) {
    if (count === 0 || actual === 0) {
        return 100;
    }
    const accuracy = 100 - (Math.abs(estimated - actual) / actual) * 100;
    return Math.max(0, Math.min(100, accuracy));
}
function computeOverEstimationPercent(actual, estimated, count) {
    if (count === 0 || actual === 0) {
        return 0;
    }
    return ((estimated - actual) / actual) * 100;
}
function emptyAggregation() {
    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,
    };
}
// ---------------------------------------------------------------------------
// Multi-month entry loading (for all_time filter)
// ---------------------------------------------------------------------------
function loadAllMonthEntries(projectHash, dataDir) {
    const months = listAvailableMonths(projectHash, dataDir);
    const all = [];
    for (const month of months) {
        const filePath = getLedgerPath(projectHash, dataDir, month);
        all.push(...readMonthFile(filePath));
    }
    return all;
}
function listAvailableMonths(projectHash, dataDir) {
    const dir = getLedgerDir(projectHash, dataDir);
    if (!existsSync(dir)) {
        return [];
    }
    try {
        return readdirSync(dir)
            .filter((f) => /^\d{4}-\d{2}\.json$/.test(f))
            .map((f) => f.slice(0, 7))
            .sort();
    }
    catch {
        return [];
    }
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
 * Append a new entry to the current month's ledger file.
 */
export async function recordEntry(projectHash, dataDir, entry) {
    const month = currentMonth();
    const filePath = getLedgerPath(projectHash, dataDir, month);
    await withFileLock(filePath, () => {
        const entries = readMonthFile(filePath);
        entries.push(entry);
        writeMonthFile(filePath, entries);
        return Promise.resolve();
    });
}
/**
 * Load and filter ledger entries according to the given filter criteria.
 */
export function getEntries(projectHash, dataDir, filter) {
    let entries;
    if (!filter.period || filter.period === 'all_time') {
        entries = loadAllMonthEntries(projectHash, dataDir);
    }
    else {
        const month = currentMonth();
        const filePath = getLedgerPath(projectHash, dataDir, month);
        entries = readMonthFile(filePath);
    }
    entries = filterByPeriod(entries, filter.period);
    if (filter.specId) {
        entries = entries.filter((e) => e.specId === filter.specId);
    }
    if (filter.toolName) {
        entries = entries.filter((e) => e.toolName === filter.toolName);
    }
    if (filter.model) {
        entries = entries.filter((e) => e.model === filter.model);
    }
    if (filter.sessionId) {
        entries = entries.filter((e) => e.sessionId === filter.sessionId);
    }
    return Promise.resolve(entries);
}
/**
 * Compute aggregated statistics for filtered ledger entries.
 */
export async function getAggregation(projectHash, dataDir, filter) {
    const entries = await getEntries(projectHash, dataDir, filter);
    return computeAggregation(entries);
}
/**
 * Load the current month's ledger including recomputed aggregation.
 */
export function getCurrentMonthLedger(projectHash, dataDir) {
    const month = currentMonth();
    const filePath = getLedgerPath(projectHash, dataDir, month);
    const entries = readMonthFile(filePath);
    return Promise.resolve({
        month,
        entries,
        aggregation: computeAggregation(entries),
    });
}
/**
 * Return the list of months for which ledger files exist (YYYY-MM format).
 */
export function getHistoricalMonths(projectHash, dataDir) {
    return Promise.resolve(listAvailableMonths(projectHash, dataDir));
}
/**
 * Archive months older than ROTATION_MONTHS into a summary.json file.
 */
export async function rotateLedger(projectHash, dataDir) {
    const months = listAvailableMonths(projectHash, dataDir);
    const cutoff = new Date();
    cutoff.setMonth(cutoff.getMonth() - ROTATION_MONTHS);
    const cutoffStr = cutoff.toISOString().slice(0, 7);
    const toArchive = months.filter((m) => m < cutoffStr);
    if (toArchive.length === 0) {
        return;
    }
    const summaryPath = getSummaryPath(projectHash, dataDir);
    await withFileLock(summaryPath, () => {
        const existing = readSummary(summaryPath);
        for (const month of toArchive) {
            const filePath = getLedgerPath(projectHash, dataDir, month);
            const entries = readMonthFile(filePath);
            const agg = computeAggregation(entries);
            existing[month] = agg;
            // Remove the detailed file after archiving
            try {
                writeFileSync(filePath, '[]', 'utf-8');
            }
            catch {
                // ignore
            }
        }
        writeFileSync(summaryPath, JSON.stringify(existing, null, 2), 'utf-8');
        return Promise.resolve();
    });
}
function readSummary(summaryPath) {
    if (!existsSync(summaryPath)) {
        return {};
    }
    try {
        const raw = readFileSync(summaryPath, 'utf-8');
        const parsed = JSON.parse(raw);
        if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
            return {};
        }
        return parsed;
    }
    catch {
        return {};
    }
}
//# sourceMappingURL=token-ledger-store.js.map