"use strict";
/**
 * CLIProxy Usage Syncer
 *
 * Periodically fetches CLIProxy usage data, transforms it, and persists
 * snapshots locally so analytics data survives CLIProxy restarts.
 *
 * Snapshot location: ~/.ccs/cache/cliproxy-usage/latest.json
 * Sync interval: 5 minutes
 */
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.stopCliproxySync = exports.startCliproxySync = exports.syncCliproxyUsage = exports.loadCachedCliproxyData = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const stats_fetcher_1 = require("../../cliproxy/services/stats-fetcher");
const cliproxy_usage_transformer_1 = require("./cliproxy-usage-transformer");
const ui_1 = require("../../utils/ui");
const config_loader_facade_1 = require("../../config/config-loader-facade");
const SNAPSHOT_VERSION = 3;
const SNAPSHOT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
const HISTORY_RETENTION_DAYS = Math.max(30, parseInt(process.env.CCS_CLIPROXY_HISTORY_RETENTION_DAYS ?? '365', 10) || 365);
const HISTORY_RETENTION_MS = HISTORY_RETENTION_DAYS * 24 * 60 * 60 * 1000;
const MAX_WRITE_ATTEMPTS = 3;
/** Sync interval in ms, configurable via CCS_CLIPROXY_SYNC_INTERVAL env var (default: 5 min) */
const SYNC_INTERVAL_MS = Math.max(30000, parseInt(process.env.CCS_CLIPROXY_SYNC_INTERVAL ?? '300000', 10) || 300000);
let syncIntervalId = null;
let snapshotTimestampOrdinal = 0;
function getCliproxyCacheDir() {
    return path.join((0, config_loader_facade_1.getCcsDir)(), 'cache', 'cliproxy-usage');
}
function getLatestSnapshotPath() {
    return path.join(getCliproxyCacheDir(), 'latest.json');
}
function ensureCliproxyCacheDir() {
    const dir = getCliproxyCacheDir();
    if (!fs.existsSync(dir)) {
        fs.mkdirSync(dir, { recursive: true });
    }
}
function getSnapshotTimestamp() {
    snapshotTimestampOrdinal = (snapshotTimestampOrdinal + 1) % 1000;
    return Date.now() + snapshotTimestampOrdinal / 1000;
}
function buildHourlyTimestamp(hour) {
    return `${hour.replace(' ', 'T')}:00.000Z`;
}
function buildDailyTimestamp(date) {
    return `${date}T00:00:00.000Z`;
}
function distributeRequestCounts(total, buckets) {
    if (buckets <= 0)
        return [];
    const normalizedTotal = Math.max(buckets, total);
    const base = Math.floor(normalizedTotal / buckets);
    const remainder = normalizedTotal % buckets;
    return Array.from({ length: buckets }, (_value, index) => base + (index < remainder ? 1 : 0));
}
function buildLegacyHistoryDetails(snapshot) {
    const details = [];
    const coveredDailyKeys = new Set();
    for (const hour of snapshot.hourly ?? []) {
        const requestCounts = distributeRequestCounts(hour.requestCount ?? hour.modelBreakdowns.length, hour.modelBreakdowns.length);
        hour.modelBreakdowns.forEach((breakdown, index) => {
            details.push({
                model: breakdown.modelName,
                timestamp: buildHourlyTimestamp(hour.hour),
                source: hour.source,
                authIndex: 'legacy-hourly',
                inputTokens: breakdown.inputTokens,
                outputTokens: breakdown.outputTokens,
                cacheReadTokens: breakdown.cacheReadTokens,
                requestCount: requestCounts[index] ?? 1,
                cost: breakdown.cost,
                failed: false,
            });
            coveredDailyKeys.add(`${hour.hour.slice(0, 10)}|${breakdown.modelName}`);
        });
    }
    for (const day of snapshot.daily ?? []) {
        for (const breakdown of day.modelBreakdowns) {
            const key = `${day.date}|${breakdown.modelName}`;
            if (coveredDailyKeys.has(key)) {
                continue;
            }
            details.push({
                model: breakdown.modelName,
                timestamp: buildDailyTimestamp(day.date),
                source: day.source,
                authIndex: 'legacy-daily',
                inputTokens: breakdown.inputTokens,
                outputTokens: breakdown.outputTokens,
                cacheReadTokens: breakdown.cacheReadTokens,
                requestCount: 1,
                cost: breakdown.cost,
                failed: false,
            });
        }
    }
    return details;
}
function migrateLegacySnapshot(snapshot, emitWarnings) {
    const details = buildLegacyHistoryDetails(snapshot);
    if (details.length === 0) {
        if (emitWarnings) {
            console.log((0, ui_1.info)('CLIProxy legacy snapshot had no migratable history, will refresh on next sync'));
        }
        return null;
    }
    if (emitWarnings) {
        console.log((0, ui_1.info)('Loaded legacy CLIProxy snapshot into historical format'));
    }
    const { daily, hourly, monthly } = (0, cliproxy_usage_transformer_1.buildCliproxyUsageHistoryAggregates)(details);
    return {
        version: SNAPSHOT_VERSION,
        timestamp: Number.isFinite(snapshot.timestamp) ? Number(snapshot.timestamp) : Date.now(),
        details,
        daily,
        hourly,
        monthly,
    };
}
function readSnapshot(emitWarnings = true) {
    try {
        const snapshotPath = getLatestSnapshotPath();
        if (!fs.existsSync(snapshotPath)) {
            return null;
        }
        const raw = fs.readFileSync(snapshotPath, 'utf-8');
        const snapshot = JSON.parse(raw);
        if (snapshot.version === SNAPSHOT_VERSION) {
            if (!Number.isFinite(snapshot.timestamp)) {
                if (emitWarnings) {
                    console.log((0, ui_1.info)('CLIProxy snapshot timestamp invalid, will refresh on next sync'));
                }
                return null;
            }
            if (!Array.isArray(snapshot.details)) {
                if (emitWarnings) {
                    console.log((0, ui_1.info)('CLIProxy snapshot details missing, will refresh on next sync'));
                }
                return null;
            }
            return snapshot;
        }
        if (snapshot.version === 1 || snapshot.version === 2) {
            return migrateLegacySnapshot(snapshot, emitWarnings);
        }
        if (emitWarnings) {
            console.log((0, ui_1.info)('CLIProxy snapshot version mismatch, will refresh on next sync'));
        }
        return null;
    }
    catch (err) {
        if (emitWarnings) {
            console.log((0, ui_1.warn)('Failed to read CLIProxy snapshot:') + ` ${err.message}`);
        }
        return null;
    }
}
function buildSnapshot(baseDetails, incomingDetails) {
    const mergedDetails = (0, cliproxy_usage_transformer_1.pruneCliproxyUsageHistoryDetails)((0, cliproxy_usage_transformer_1.mergeCliproxyUsageHistoryDetails)(baseDetails, incomingDetails), Date.now() - HISTORY_RETENTION_MS);
    const { daily, hourly, monthly } = (0, cliproxy_usage_transformer_1.buildCliproxyUsageHistoryAggregates)(mergedDetails);
    return {
        version: SNAPSHOT_VERSION,
        timestamp: getSnapshotTimestamp(),
        details: mergedDetails,
        daily,
        hourly,
        monthly,
    };
}
async function writeSnapshotWithMerge(incomingDetails) {
    ensureCliproxyCacheDir();
    const snapshotPath = getLatestSnapshotPath();
    for (let attempt = 0; attempt < MAX_WRITE_ATTEMPTS; attempt++) {
        const baseSnapshot = readSnapshot(false);
        const baseTimestamp = baseSnapshot?.timestamp ?? -Infinity;
        const snapshot = buildSnapshot(baseSnapshot?.details ?? [], incomingDetails);
        const tempFile = `${snapshotPath}.${process.pid}.${snapshot.timestamp}.tmp`;
        fs.writeFileSync(tempFile, JSON.stringify(snapshot), 'utf-8');
        const latestSnapshot = readSnapshot(false);
        const latestTimestamp = latestSnapshot?.timestamp ?? -Infinity;
        if (latestTimestamp > baseTimestamp) {
            fs.rmSync(tempFile, { force: true });
            continue;
        }
        fs.renameSync(tempFile, snapshotPath);
        console.log((0, ui_1.ok)('CLIProxy usage snapshot updated'));
        return;
    }
    throw new Error('Failed to write CLIProxy snapshot after repeated overlap retries');
}
async function loadCachedCliproxyData() {
    const empty = { daily: [], hourly: [], monthly: [] };
    const snapshot = readSnapshot();
    if (!snapshot) {
        return empty;
    }
    const age = Date.now() - snapshot.timestamp;
    if (age > SNAPSHOT_MAX_AGE_MS) {
        console.log((0, ui_1.info)('Using stale CLIProxy snapshot while proxy sync is unavailable'));
    }
    return { daily: snapshot.daily, hourly: snapshot.hourly, monthly: snapshot.monthly };
}
exports.loadCachedCliproxyData = loadCachedCliproxyData;
async function syncCliproxyUsage(fetchRaw = stats_fetcher_1.fetchCliproxyUsageRaw) {
    const raw = await fetchRaw();
    if (raw === null) {
        console.log((0, ui_1.warn)('CLIProxy usage sync skipped: proxy unavailable'));
        return;
    }
    try {
        await writeSnapshotWithMerge((0, cliproxy_usage_transformer_1.extractCliproxyUsageHistoryDetails)(raw));
    }
    catch (err) {
        console.log((0, ui_1.warn)('Failed to write CLIProxy snapshot:') + ` ${err.message}`);
    }
}
exports.syncCliproxyUsage = syncCliproxyUsage;
function startCliproxySync(syncNow = () => syncCliproxyUsage()) {
    if (syncIntervalId !== null) {
        return;
    }
    const intervalMin = Math.round(SYNC_INTERVAL_MS / 60000);
    console.log((0, ui_1.info)(`Starting CLIProxy usage sync (interval: ${intervalMin} min)`));
    void syncNow();
    syncIntervalId = setInterval(() => {
        void syncNow();
    }, SYNC_INTERVAL_MS);
}
exports.startCliproxySync = startCliproxySync;
function stopCliproxySync() {
    if (syncIntervalId !== null) {
        clearInterval(syncIntervalId);
        syncIntervalId = null;
    }
}
exports.stopCliproxySync = stopCliproxySync;
//# sourceMappingURL=cliproxy-usage-syncer.js.map