/**
 * Proxy usage statistics with restart- and handoff-safe persistence.
 *
 * Request handlers update memory synchronously. Small deltas are merged into a
 * shared snapshot asynchronously, under a cross-process lock, so overlapping
 * rolling workers cannot overwrite each other's counters.
 */
import { randomUUID } from "node:crypto";
import { mkdir, open, readFile, readdir, rename, rm, stat, } from "node:fs/promises";
import { basename, dirname, join } from "node:path";
import { AsyncMutex } from "../utils/asyncMutex.js";
import { redactUrlsInText, sanitizeForLog } from "../utils/logSanitize.js";
import { writeJsonSnapshotAtomically } from "./snapshotPersistence.js";
const SNAPSHOT_SCHEMA_VERSION = 1;
const DEFAULT_FLUSH_INTERVAL_MS = 1_000;
const DEFAULT_LOCK_TIMEOUT_MS = 2_000;
const DEFAULT_STALE_LOCK_MS = 30_000;
const LOCK_RETRY_MS = 25;
const MAX_CORRUPT_SNAPSHOTS = 3;
const MAX_RECENT_TERMINAL_ERRORS = 20;
const MAX_TERMINAL_ERROR_FIELD_LENGTH = 128;
const MAX_TERMINAL_ERROR_MESSAGE_LENGTH = 500;
const TERMINAL_ERROR_ESCAPE = String.fromCharCode(27);
const TERMINAL_ERROR_BELL = String.fromCharCode(7);
const TERMINAL_ERROR_OSC_PATTERN = new RegExp(`${TERMINAL_ERROR_ESCAPE}\\][\\s\\S]*?(?:${TERMINAL_ERROR_BELL}|${TERMINAL_ERROR_ESCAPE}\\\\)`, "g");
const TERMINAL_ERROR_ANSI_PATTERN = new RegExp(`${TERMINAL_ERROR_ESCAPE}\\[[0-?]*[ -/]*[@-~]`, "g");
const TERMINAL_ERROR_CATEGORIES = [
    "authentication",
    "client_cancelled",
    "fallback_exhausted",
    "invalid_request",
    "proxy_error",
    "rate_limit",
    "stream_error",
    "unclassified",
    "upstream_error",
    "other",
];
class InvalidProxyStatsSnapshotError extends Error {
}
function emptyStats(startedAt) {
    return {
        startedAt,
        totalAttempts: 0,
        totalAttemptErrors: 0,
        totalRequests: 0,
        totalSuccess: 0,
        totalErrors: 0,
        totalRateLimits: 0,
        totalTransientRateLimits: 0,
        totalQuotaRateLimits: 0,
        accounts: {},
    };
}
function emptyTerminalErrorCounts() {
    return Object.fromEntries(TERMINAL_ERROR_CATEGORIES.map((category) => [category, 0]));
}
function emptyTerminalErrorJournal(startedAt) {
    return {
        startedAt,
        totalErrors: 0,
        counts: emptyTerminalErrorCounts(),
        recent: [],
    };
}
function cloneTerminalErrorSummary(summary) {
    return { ...summary };
}
function cloneTerminalErrorJournal(journal) {
    return {
        ...journal,
        counts: { ...journal.counts },
        recent: journal.recent.map(cloneTerminalErrorSummary),
    };
}
function mergeTerminalErrorJournals(left, right) {
    const counts = emptyTerminalErrorCounts();
    for (const category of TERMINAL_ERROR_CATEGORIES) {
        counts[category] = left.counts[category] + right.counts[category];
    }
    const byId = new Map();
    for (const summary of [...left.recent, ...right.recent]) {
        byId.set(summary.id, cloneTerminalErrorSummary(summary));
    }
    const recent = [...byId.values()]
        .sort((a, b) => a.at - b.at || a.id.localeCompare(b.id))
        .slice(-MAX_RECENT_TERMINAL_ERRORS);
    return {
        startedAt: Math.min(left.startedAt, right.startedAt),
        totalErrors: left.totalErrors + right.totalErrors,
        counts,
        recent,
    };
}
function terminalErrorsPathForStats(filePath) {
    return join(dirname(filePath), "proxy-terminal-errors.json");
}
function clipTerminalErrorField(value) {
    const clipped = value?.slice(0, MAX_TERMINAL_ERROR_FIELD_LENGTH);
    return clipped ? clipped : undefined;
}
function stripTerminalErrorControlCharacters(value) {
    const withoutAnsi = value
        .replace(TERMINAL_ERROR_OSC_PATTERN, "")
        .replace(TERMINAL_ERROR_ANSI_PATTERN, "");
    return Array.from(withoutAnsi, (character) => {
        const code = character.charCodeAt(0);
        return code < 32 || (code >= 127 && code <= 159) ? " " : character;
    }).join("");
}
function terminalErrorCategory(status, errorType) {
    const normalized = errorType?.toLowerCase() ?? "";
    if (status === 499 || normalized.includes("client_cancel")) {
        return "client_cancelled";
    }
    if (normalized.includes("stream_telemetry")) {
        return "proxy_error";
    }
    if (normalized.includes("stream")) {
        return "stream_error";
    }
    if (status === 429 || normalized.includes("rate_limit")) {
        return "rate_limit";
    }
    if (normalized.includes("auth") || normalized.includes("token_refresh")) {
        return "authentication";
    }
    if (status === 404 ||
        normalized.includes("invalid_request") ||
        normalized.includes("not_found") ||
        normalized.includes("construction_rejection")) {
        return "invalid_request";
    }
    if (normalized.includes("fallback_exhausted")) {
        return "fallback_exhausted";
    }
    if (normalized.includes("proxy") ||
        normalized.includes("handler_error") ||
        normalized.includes("runtime")) {
        return "proxy_error";
    }
    if (normalized.includes("upstream") ||
        normalized.includes("network") ||
        normalized.includes("transient") ||
        normalized.includes("generation") ||
        normalized.includes("all_accounts") ||
        normalized === "api_error" ||
        status >= 500) {
        return "upstream_error";
    }
    return normalized ? "other" : "unclassified";
}
function createTerminalErrorSummary(args) {
    const { now, status, accountLabel, accountType, details } = args;
    const errorType = clipTerminalErrorField(details?.errorType);
    const message = details?.message
        ? stripTerminalErrorControlCharacters(sanitizeForLog(redactUrlsInText(details.message), MAX_TERMINAL_ERROR_MESSAGE_LENGTH + MAX_TERMINAL_ERROR_FIELD_LENGTH))
            .replace(/\s+/g, " ")
            .trim()
            .slice(0, MAX_TERMINAL_ERROR_MESSAGE_LENGTH)
        : undefined;
    return {
        id: randomUUID(),
        at: now,
        status,
        category: terminalErrorCategory(status, errorType),
        ...(clipTerminalErrorField(details?.requestId)
            ? { requestId: clipTerminalErrorField(details?.requestId) }
            : {}),
        ...(clipTerminalErrorField(accountLabel)
            ? { account: clipTerminalErrorField(accountLabel) }
            : {}),
        ...(clipTerminalErrorField(accountType)
            ? { accountType: clipTerminalErrorField(accountType) }
            : {}),
        ...(errorType ? { errorType } : {}),
        ...(clipTerminalErrorField(details?.errorCode)
            ? { errorCode: clipTerminalErrorField(details?.errorCode) }
            : {}),
        ...(clipTerminalErrorField(details?.terminalOutcome)
            ? {
                terminalOutcome: clipTerminalErrorField(details?.terminalOutcome),
            }
            : {}),
        ...(message ? { message } : {}),
    };
}
function cloneAccount(account) {
    return { ...account };
}
function cloneStats(value) {
    return {
        ...value,
        accounts: Object.fromEntries(Object.entries(value.accounts).map(([label, account]) => [
            label,
            cloneAccount(account),
        ])),
    };
}
function mergeAccountStats(left, right) {
    if (!left) {
        return cloneAccount(right);
    }
    return {
        label: right.label || left.label,
        type: right.type || left.type,
        attemptCount: left.attemptCount + right.attemptCount,
        attemptErrorCount: left.attemptErrorCount + right.attemptErrorCount,
        successCount: left.successCount + right.successCount,
        errorCount: left.errorCount + right.errorCount,
        rateLimitCount: left.rateLimitCount + right.rateLimitCount,
        transientRateLimitCount: left.transientRateLimitCount + right.transientRateLimitCount,
        quotaRateLimitCount: left.quotaRateLimitCount + right.quotaRateLimitCount,
        lastAttemptAt: Math.max(left.lastAttemptAt, right.lastAttemptAt),
        ...(left.lastErrorAt || right.lastErrorAt
            ? {
                lastErrorAt: Math.max(left.lastErrorAt ?? 0, right.lastErrorAt ?? 0),
            }
            : {}),
    };
}
function mergeStats(left, right) {
    const accounts = Object.fromEntries(Object.entries(left.accounts).map(([label, account]) => [
        label,
        cloneAccount(account),
    ]));
    for (const [label, account] of Object.entries(right.accounts)) {
        accounts[label] = mergeAccountStats(accounts[label], account);
    }
    return {
        startedAt: Math.min(left.startedAt, right.startedAt),
        totalAttempts: left.totalAttempts + right.totalAttempts,
        totalAttemptErrors: left.totalAttemptErrors + right.totalAttemptErrors,
        totalRequests: left.totalRequests + right.totalRequests,
        totalSuccess: left.totalSuccess + right.totalSuccess,
        totalErrors: left.totalErrors + right.totalErrors,
        totalRateLimits: left.totalRateLimits + right.totalRateLimits,
        totalTransientRateLimits: left.totalTransientRateLimits + right.totalTransientRateLimits,
        totalQuotaRateLimits: left.totalQuotaRateLimits + right.totalQuotaRateLimits,
        accounts,
    };
}
function finiteNonNegativeInteger(value) {
    return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
}
function validAccountStats(value) {
    if (!value || typeof value !== "object") {
        return false;
    }
    const candidate = value;
    return (typeof candidate.label === "string" &&
        typeof candidate.type === "string" &&
        finiteNonNegativeInteger(candidate.attemptCount) &&
        finiteNonNegativeInteger(candidate.attemptErrorCount) &&
        finiteNonNegativeInteger(candidate.successCount) &&
        finiteNonNegativeInteger(candidate.errorCount) &&
        finiteNonNegativeInteger(candidate.rateLimitCount) &&
        finiteNonNegativeInteger(candidate.transientRateLimitCount) &&
        finiteNonNegativeInteger(candidate.quotaRateLimitCount) &&
        finiteNonNegativeInteger(candidate.lastAttemptAt) &&
        (candidate.lastErrorAt === undefined ||
            finiteNonNegativeInteger(candidate.lastErrorAt)));
}
function validStats(value) {
    if (!value || typeof value !== "object") {
        return false;
    }
    const candidate = value;
    const counters = [
        candidate.startedAt,
        candidate.totalAttempts,
        candidate.totalAttemptErrors,
        candidate.totalRequests,
        candidate.totalSuccess,
        candidate.totalErrors,
        candidate.totalRateLimits,
        candidate.totalTransientRateLimits,
        candidate.totalQuotaRateLimits,
    ];
    if (!counters.every(finiteNonNegativeInteger) ||
        !candidate.accounts ||
        typeof candidate.accounts !== "object") {
        return false;
    }
    return Object.entries(candidate.accounts).every(([label, account]) => validAccountStats(account) && label === account.label);
}
function validOptionalString(value, maxLength) {
    return (value === undefined ||
        (typeof value === "string" && value.length <= maxLength));
}
function validTerminalErrorSummary(value) {
    if (!value || typeof value !== "object") {
        return false;
    }
    const candidate = value;
    return (typeof candidate.id === "string" &&
        candidate.id.length > 0 &&
        candidate.id.length <= MAX_TERMINAL_ERROR_FIELD_LENGTH &&
        finiteNonNegativeInteger(candidate.at) &&
        finiteNonNegativeInteger(candidate.status) &&
        typeof candidate.category === "string" &&
        TERMINAL_ERROR_CATEGORIES.includes(candidate.category) &&
        validOptionalString(candidate.requestId, MAX_TERMINAL_ERROR_FIELD_LENGTH) &&
        validOptionalString(candidate.account, MAX_TERMINAL_ERROR_FIELD_LENGTH) &&
        validOptionalString(candidate.accountType, MAX_TERMINAL_ERROR_FIELD_LENGTH) &&
        validOptionalString(candidate.errorType, MAX_TERMINAL_ERROR_FIELD_LENGTH) &&
        validOptionalString(candidate.errorCode, MAX_TERMINAL_ERROR_FIELD_LENGTH) &&
        validOptionalString(candidate.terminalOutcome, MAX_TERMINAL_ERROR_FIELD_LENGTH) &&
        validOptionalString(candidate.message, MAX_TERMINAL_ERROR_MESSAGE_LENGTH));
}
function validTerminalErrorJournal(value) {
    if (!value || typeof value !== "object") {
        return false;
    }
    const candidate = value;
    if (!finiteNonNegativeInteger(candidate.startedAt) ||
        !finiteNonNegativeInteger(candidate.totalErrors) ||
        !candidate.counts ||
        typeof candidate.counts !== "object" ||
        !Array.isArray(candidate.recent) ||
        candidate.recent.length > MAX_RECENT_TERMINAL_ERRORS ||
        !candidate.recent.every(validTerminalErrorSummary)) {
        return false;
    }
    const countEntries = Object.entries(candidate.counts);
    return (countEntries.length === TERMINAL_ERROR_CATEGORIES.length &&
        countEntries.every(([category, count]) => TERMINAL_ERROR_CATEGORIES.includes(category) && finiteNonNegativeInteger(count)) &&
        countEntries.reduce((total, [, count]) => total + Number(count), 0) ===
            candidate.totalErrors);
}
function validSnapshot(value) {
    if (!value || typeof value !== "object") {
        return false;
    }
    const candidate = value;
    return (candidate.schemaVersion === SNAPSHOT_SCHEMA_VERSION &&
        finiteNonNegativeInteger(candidate.revision) &&
        finiteNonNegativeInteger(candidate.updatedAt) &&
        validStats(candidate.stats));
}
function validTerminalErrorSnapshot(value) {
    if (!value || typeof value !== "object") {
        return false;
    }
    const candidate = value;
    return (candidate.schemaVersion === SNAPSHOT_SCHEMA_VERSION &&
        finiteNonNegativeInteger(candidate.revision) &&
        finiteNonNegativeInteger(candidate.updatedAt) &&
        validTerminalErrorJournal(candidate.journal));
}
function isMissingFileError(error) {
    return error?.code === "ENOENT";
}
function isCorruptSnapshotError(error) {
    return (error instanceof SyntaxError ||
        error instanceof InvalidProxyStatsSnapshotError);
}
function processIsRunning(pid) {
    try {
        process.kill(pid, 0);
        return true;
    }
    catch (error) {
        return error.code === "EPERM";
    }
}
async function sleep(ms) {
    await new Promise((resolve) => setTimeout(resolve, ms));
}
async function readLockOwner(lockPath) {
    try {
        const parsed = JSON.parse(await readFile(lockPath, "utf8"));
        if (!parsed || typeof parsed !== "object") {
            return null;
        }
        const candidate = parsed;
        if (typeof candidate.token !== "string" ||
            !finiteNonNegativeInteger(candidate.pid) ||
            candidate.pid === 0 ||
            !finiteNonNegativeInteger(candidate.acquiredAt)) {
            return null;
        }
        return candidate;
    }
    catch {
        return null;
    }
}
async function removeAbandonedLock(lockPath, staleLockMs, now) {
    const owner = await readLockOwner(lockPath);
    try {
        const lockStat = await stat(lockPath);
        const oldEnough = now - lockStat.mtimeMs >= staleLockMs;
        const ownerIsRunning = owner ? processIsRunning(owner.pid) : false;
        if (owner && ownerIsRunning && !oldEnough) {
            return false;
        }
        if ((owner && !ownerIsRunning) || oldEnough) {
            await rm(lockPath, { force: true });
            return true;
        }
    }
    catch (error) {
        return isMissingFileError(error);
    }
    return false;
}
async function acquireFileLock(lockPath, timeoutMs, staleLockMs, now) {
    await mkdir(dirname(lockPath), { recursive: true, mode: 0o700 });
    const deadline = Date.now() + timeoutMs;
    while (true) {
        const owner = {
            token: randomUUID(),
            pid: process.pid,
            acquiredAt: now(),
        };
        let handle;
        try {
            handle = await open(lockPath, "wx", 0o600);
            await handle.writeFile(JSON.stringify(owner));
            const acquiredHandle = handle;
            return async () => {
                await acquiredHandle.close().catch(() => undefined);
                const current = await readLockOwner(lockPath);
                if (current?.token === owner.token) {
                    await rm(lockPath, { force: true }).catch(() => undefined);
                }
            };
        }
        catch (error) {
            if (handle) {
                await handle.close().catch(() => undefined);
                await rm(lockPath, { force: true }).catch(() => undefined);
            }
            if (error.code !== "EEXIST") {
                throw error;
            }
        }
        if (await removeAbandonedLock(lockPath, staleLockMs, now())) {
            continue;
        }
        if (Date.now() >= deadline) {
            throw new Error(`Timed out acquiring proxy stats lock ${lockPath}`);
        }
        await sleep(LOCK_RETRY_MS);
    }
}
export class ProxyUsageStatsStore {
    now;
    flushIntervalMs;
    lockTimeoutMs;
    staleLockMs;
    filePath;
    terminalErrorsFilePath;
    stats;
    pending;
    terminalErrors;
    pendingTerminalErrors;
    pendingMutations = 0;
    inFlightMutations = 0;
    pendingTerminalErrorMutations = 0;
    inFlightTerminalErrorMutations = 0;
    revision = 0;
    terminalErrorsRevision = 0;
    flushTimer = null;
    flushMutex = new AsyncMutex();
    lastFlushedAt;
    lastReconciledAt;
    lastRecoveryAt;
    lastError;
    terminalErrorsLastFlushedAt;
    terminalErrorsLastRecoveryAt;
    terminalErrorsLastError;
    snapshotVersion = 0;
    constructor(options = {}) {
        this.now = options.now ?? Date.now;
        this.flushIntervalMs = options.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;
        this.lockTimeoutMs = options.lockTimeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;
        this.staleLockMs = options.staleLockMs ?? DEFAULT_STALE_LOCK_MS;
        this.filePath = options.filePath;
        this.terminalErrorsFilePath =
            options.terminalErrorsFilePath ??
                (options.filePath
                    ? terminalErrorsPathForStats(options.filePath)
                    : undefined);
        const startedAt = this.now();
        this.stats = emptyStats(startedAt);
        this.pending = emptyStats(startedAt);
        this.terminalErrors = emptyTerminalErrorJournal(startedAt);
        this.pendingTerminalErrors = emptyTerminalErrorJournal(startedAt);
    }
    async initialize(filePath = this.filePath ?? "") {
        this.cancelFlushTimer();
        const configuredStatsPath = this.filePath;
        const configuredTerminalErrorsPath = this.terminalErrorsFilePath;
        this.filePath = filePath || undefined;
        this.terminalErrorsFilePath =
            this.filePath &&
                configuredStatsPath === this.filePath &&
                configuredTerminalErrorsPath
                ? configuredTerminalErrorsPath
                : this.filePath
                    ? terminalErrorsPathForStats(this.filePath)
                    : undefined;
        const startedAt = this.now();
        this.stats = emptyStats(startedAt);
        this.pending = emptyStats(startedAt);
        this.terminalErrors = emptyTerminalErrorJournal(startedAt);
        this.pendingTerminalErrors = emptyTerminalErrorJournal(startedAt);
        this.pendingMutations = 0;
        this.inFlightMutations = 0;
        this.pendingTerminalErrorMutations = 0;
        this.inFlightTerminalErrorMutations = 0;
        this.revision = 0;
        this.terminalErrorsRevision = 0;
        this.lastFlushedAt = undefined;
        this.lastReconciledAt = undefined;
        this.lastRecoveryAt = undefined;
        this.lastError = undefined;
        this.terminalErrorsLastFlushedAt = undefined;
        this.terminalErrorsLastRecoveryAt = undefined;
        this.terminalErrorsLastError = undefined;
        this.snapshotVersion = 0;
        if (!this.filePath) {
            return;
        }
        try {
            const snapshot = await this.readSnapshot();
            if (snapshot) {
                this.stats = cloneStats(snapshot.stats);
                this.pending = emptyStats(this.now());
                this.revision = snapshot.revision;
                this.lastFlushedAt = snapshot.updatedAt;
            }
            this.lastReconciledAt = this.now();
        }
        catch (error) {
            if (isCorruptSnapshotError(error)) {
                try {
                    const recoveredSnapshot = await this.recoverCorruptSnapshot();
                    if (recoveredSnapshot) {
                        this.applySnapshot(recoveredSnapshot);
                    }
                    this.lastReconciledAt = this.now();
                }
                catch (recoveryError) {
                    this.lastError = this.describeError(recoveryError);
                }
            }
            else {
                this.lastError = this.describeError(error);
            }
        }
        if (!this.terminalErrorsFilePath) {
            return;
        }
        try {
            const snapshot = await this.readTerminalErrorSnapshot();
            if (snapshot) {
                this.applyTerminalErrorSnapshot(snapshot);
            }
        }
        catch (error) {
            if (isCorruptSnapshotError(error)) {
                try {
                    const recoveredSnapshot = await this.recoverCorruptTerminalErrorSnapshot();
                    if (recoveredSnapshot) {
                        this.applyTerminalErrorSnapshot(recoveredSnapshot);
                    }
                }
                catch (recoveryError) {
                    this.terminalErrorsLastError = this.describeError(recoveryError);
                }
            }
            else {
                this.terminalErrorsLastError = this.describeError(error);
            }
        }
    }
    recordAttempt(accountLabel, accountType) {
        const attemptedAt = this.now();
        this.applyAttempt(this.stats, accountLabel, accountType, attemptedAt);
        this.applyAttempt(this.pending, accountLabel, accountType, attemptedAt);
        this.markMutation();
    }
    recordFinalSuccess(accountLabel, accountType) {
        this.applyFinalSuccess(this.stats, accountLabel, accountType);
        this.applyFinalSuccess(this.pending, accountLabel, accountType);
        this.markMutation();
    }
    recordAttemptError(accountLabel, accountType, status, rateLimitKind) {
        const failedAt = this.now();
        this.applyAttemptError(this.stats, accountLabel, accountType, status, rateLimitKind, failedAt);
        this.applyAttemptError(this.pending, accountLabel, accountType, status, rateLimitKind, failedAt);
        this.markMutation();
    }
    recordFinalError(status, accountLabel, accountType, details) {
        const failedAt = this.now();
        const summary = createTerminalErrorSummary({
            now: failedAt,
            status,
            accountLabel,
            accountType,
            details,
        });
        this.applyFinalError(this.stats, accountLabel, accountType, failedAt);
        this.applyFinalError(this.pending, accountLabel, accountType, failedAt);
        this.applyTerminalError(this.terminalErrors, summary);
        if (this.terminalErrorsFilePath) {
            this.applyTerminalError(this.pendingTerminalErrors, summary);
            this.pendingTerminalErrorMutations += 1;
        }
        this.markMutation();
    }
    getStats() {
        return cloneStats(this.stats);
    }
    getAccountStats(label) {
        const account = this.stats.accounts[label];
        return account ? cloneAccount(account) : undefined;
    }
    getTerminalErrors() {
        return cloneTerminalErrorJournal(this.terminalErrors);
    }
    getUsageSnapshot() {
        const version = this.snapshotVersion;
        return {
            stats: this.getStats(),
            statsVersion: version,
            terminalErrors: this.getTerminalErrors(),
            terminalErrorsVersion: version,
        };
    }
    getPersistenceStatus() {
        return {
            enabled: !!this.filePath,
            filePath: this.filePath ?? null,
            revision: this.revision,
            pendingMutations: this.pendingMutations,
            inFlightMutations: this.inFlightMutations,
            unpersistedMutations: this.pendingMutations + this.inFlightMutations,
            lastFlushedAt: this.lastFlushedAt ?? null,
            lastReconciledAt: this.lastReconciledAt ?? null,
            lastRecoveryAt: this.lastRecoveryAt ?? null,
            lastError: this.lastError ?? null,
            terminalErrorsFilePath: this.terminalErrorsFilePath ?? null,
            terminalErrorsRevision: this.terminalErrorsRevision,
            terminalErrorsPending: this.pendingTerminalErrorMutations,
            terminalErrorsInFlight: this.inFlightTerminalErrorMutations,
            terminalErrorsUnpersisted: this.pendingTerminalErrorMutations +
                this.inFlightTerminalErrorMutations,
            terminalErrorsLastFlushedAt: this.terminalErrorsLastFlushedAt ?? null,
            terminalErrorsLastRecoveryAt: this.terminalErrorsLastRecoveryAt ?? null,
            terminalErrorsLastError: this.terminalErrorsLastError ?? null,
        };
    }
    async flush() {
        if (!this.filePath ||
            (this.pendingMutations === 0 &&
                this.inFlightMutations === 0 &&
                this.pendingTerminalErrorMutations === 0 &&
                this.inFlightTerminalErrorMutations === 0)) {
            return;
        }
        this.cancelFlushTimer();
        await this.flushMutex.runExclusive(async () => {
            if (!this.filePath) {
                return;
            }
            let statsDelta;
            let terminalErrorDelta;
            if (this.pendingMutations > 0) {
                statsDelta = {
                    value: this.pending,
                    mutations: this.pendingMutations,
                };
                this.pending = emptyStats(this.now());
                this.pendingMutations = 0;
                this.inFlightMutations = statsDelta.mutations;
            }
            if (this.terminalErrorsFilePath &&
                this.pendingTerminalErrorMutations > 0) {
                terminalErrorDelta = {
                    value: this.pendingTerminalErrors,
                    mutations: this.pendingTerminalErrorMutations,
                };
                this.pendingTerminalErrors = emptyTerminalErrorJournal(this.now());
                this.pendingTerminalErrorMutations = 0;
                this.inFlightTerminalErrorMutations = terminalErrorDelta.mutations;
            }
            const countersPersisted = statsDelta
                ? await this.persistStatsDelta(statsDelta.value, statsDelta.mutations)
                : true;
            if (terminalErrorDelta) {
                if (countersPersisted) {
                    await this.persistTerminalErrorDelta(terminalErrorDelta.value, terminalErrorDelta.mutations);
                }
                else {
                    this.pendingTerminalErrors = mergeTerminalErrorJournals(terminalErrorDelta.value, this.pendingTerminalErrors);
                    this.pendingTerminalErrorMutations += terminalErrorDelta.mutations;
                    this.inFlightTerminalErrorMutations = 0;
                }
            }
            if (this.pendingMutations > 0 || this.pendingTerminalErrorMutations > 0) {
                this.scheduleFlush();
            }
        });
    }
    async persistStatsDelta(delta, deltaMutations) {
        if (!this.filePath) {
            return false;
        }
        let releaseLock;
        try {
            releaseLock = await acquireFileLock(`${this.filePath}.lock`, this.lockTimeoutMs, this.staleLockMs, this.now);
            let existing;
            try {
                existing = await this.readSnapshot();
            }
            catch (error) {
                if (!isCorruptSnapshotError(error)) {
                    throw error;
                }
                await this.quarantineCorruptSnapshot();
                existing = null;
            }
            const persisted = existing?.stats ?? emptyStats(delta.startedAt);
            const merged = mergeStats(persisted, delta);
            const revision = (existing?.revision ?? 0) + 1;
            const updatedAt = this.now();
            await writeJsonSnapshotAtomically(this.filePath, {
                schemaVersion: SNAPSHOT_SCHEMA_VERSION,
                revision,
                updatedAt,
                stats: merged,
            }, 0o600);
            this.revision = revision;
            this.lastFlushedAt = updatedAt;
            this.lastReconciledAt = updatedAt;
            this.lastError = undefined;
            this.stats = mergeStats(merged, this.pending);
            this.inFlightMutations = 0;
            return true;
        }
        catch (error) {
            this.pending = mergeStats(delta, this.pending);
            this.pendingMutations += deltaMutations;
            this.inFlightMutations = 0;
            this.lastError = this.describeError(error);
            return false;
        }
        finally {
            await releaseLock?.();
        }
    }
    async persistTerminalErrorDelta(delta, deltaMutations) {
        if (!this.terminalErrorsFilePath) {
            return;
        }
        let releaseLock;
        try {
            releaseLock = await acquireFileLock(`${this.terminalErrorsFilePath}.lock`, this.lockTimeoutMs, this.staleLockMs, this.now);
            let existing;
            try {
                existing = await this.readTerminalErrorSnapshot();
            }
            catch (error) {
                if (!isCorruptSnapshotError(error)) {
                    throw error;
                }
                await this.quarantineCorruptTerminalErrorSnapshot();
                existing = null;
            }
            const persisted = existing?.journal ?? emptyTerminalErrorJournal(delta.startedAt);
            const merged = mergeTerminalErrorJournals(persisted, delta);
            const revision = (existing?.revision ?? 0) + 1;
            const updatedAt = this.now();
            await writeJsonSnapshotAtomically(this.terminalErrorsFilePath, {
                schemaVersion: SNAPSHOT_SCHEMA_VERSION,
                revision,
                updatedAt,
                journal: merged,
            }, 0o600);
            this.terminalErrorsRevision = revision;
            this.terminalErrorsLastFlushedAt = updatedAt;
            this.terminalErrorsLastError = undefined;
            this.terminalErrors = mergeTerminalErrorJournals(merged, this.pendingTerminalErrors);
            this.inFlightTerminalErrorMutations = 0;
        }
        catch (error) {
            this.pendingTerminalErrors = mergeTerminalErrorJournals(delta, this.pendingTerminalErrors);
            this.pendingTerminalErrorMutations += deltaMutations;
            this.inFlightTerminalErrorMutations = 0;
            this.terminalErrorsLastError = this.describeError(error);
        }
        finally {
            await releaseLock?.();
        }
    }
    async reconcileUsageSnapshot() {
        if (!this.filePath) {
            return this.getUsageSnapshot();
        }
        return this.flushMutex.runExclusive(async () => {
            try {
                const snapshot = await this.readSnapshot();
                if (snapshot) {
                    this.stats = mergeStats(snapshot.stats, this.pending);
                    this.revision = snapshot.revision;
                    this.lastFlushedAt = snapshot.updatedAt;
                }
                this.lastReconciledAt = this.now();
                this.lastError = undefined;
            }
            catch (error) {
                this.lastError = this.describeError(error);
            }
            if (this.terminalErrorsFilePath) {
                try {
                    const snapshot = await this.readTerminalErrorSnapshot();
                    if (snapshot) {
                        this.terminalErrors = mergeTerminalErrorJournals(snapshot.journal, this.pendingTerminalErrors);
                        this.terminalErrorsRevision = snapshot.revision;
                        this.terminalErrorsLastFlushedAt = snapshot.updatedAt;
                    }
                    this.terminalErrorsLastError = undefined;
                }
                catch (error) {
                    this.terminalErrorsLastError = this.describeError(error);
                }
            }
            this.snapshotVersion += 1;
            return this.getUsageSnapshot();
        });
    }
    async reconcile() {
        return (await this.reconcileUsageSnapshot()).stats;
    }
    resetMemory() {
        this.cancelFlushTimer();
        const startedAt = this.now();
        this.stats = emptyStats(startedAt);
        this.pending = emptyStats(startedAt);
        this.terminalErrors = emptyTerminalErrorJournal(startedAt);
        this.pendingTerminalErrors = emptyTerminalErrorJournal(startedAt);
        this.pendingMutations = 0;
        this.inFlightMutations = 0;
        this.pendingTerminalErrorMutations = 0;
        this.inFlightTerminalErrorMutations = 0;
        this.revision = 0;
        this.terminalErrorsRevision = 0;
        this.lastFlushedAt = undefined;
        this.lastReconciledAt = undefined;
        this.lastRecoveryAt = undefined;
        this.lastError = undefined;
        this.terminalErrorsLastFlushedAt = undefined;
        this.terminalErrorsLastRecoveryAt = undefined;
        this.terminalErrorsLastError = undefined;
        this.snapshotVersion = 0;
    }
    async resetForTests() {
        await this.flushMutex.runExclusive(async () => {
            this.resetMemory();
            this.filePath = undefined;
            this.terminalErrorsFilePath = undefined;
        });
    }
    markMutation() {
        this.pendingMutations += 1;
        this.snapshotVersion += 1;
        this.scheduleFlush();
    }
    applyAttempt(target, accountLabel, accountType, attemptedAt) {
        target.totalAttempts += 1;
        const account = this.ensureAccount(target, accountLabel, accountType);
        account.attemptCount += 1;
        account.lastAttemptAt = attemptedAt;
    }
    applyFinalSuccess(target, accountLabel, accountType) {
        target.totalRequests += 1;
        target.totalSuccess += 1;
        if (accountLabel && accountType) {
            this.ensureAccount(target, accountLabel, accountType).successCount += 1;
        }
    }
    applyAttemptError(target, accountLabel, accountType, status, rateLimitKind, failedAt) {
        const account = this.ensureAccount(target, accountLabel, accountType);
        target.totalAttemptErrors += 1;
        account.attemptErrorCount += 1;
        account.lastErrorAt = failedAt;
        if (status !== 429) {
            return;
        }
        target.totalRateLimits += 1;
        account.rateLimitCount += 1;
        if (rateLimitKind === "transient") {
            target.totalTransientRateLimits += 1;
            account.transientRateLimitCount += 1;
        }
        else if (rateLimitKind === "quota") {
            target.totalQuotaRateLimits += 1;
            account.quotaRateLimitCount += 1;
        }
    }
    applyFinalError(target, accountLabel, accountType, failedAt) {
        target.totalRequests += 1;
        target.totalErrors += 1;
        if (accountLabel && accountType) {
            const account = this.ensureAccount(target, accountLabel, accountType);
            account.errorCount += 1;
            account.lastErrorAt = failedAt;
        }
    }
    applyTerminalError(target, summary) {
        target.totalErrors += 1;
        target.counts[summary.category] += 1;
        target.recent.push(cloneTerminalErrorSummary(summary));
        if (target.recent.length > MAX_RECENT_TERMINAL_ERRORS) {
            target.recent.splice(0, target.recent.length - MAX_RECENT_TERMINAL_ERRORS);
        }
    }
    ensureAccount(target, label, type) {
        if (!target.accounts[label]) {
            target.accounts[label] = {
                label,
                type,
                attemptCount: 0,
                attemptErrorCount: 0,
                successCount: 0,
                errorCount: 0,
                rateLimitCount: 0,
                transientRateLimitCount: 0,
                quotaRateLimitCount: 0,
                lastAttemptAt: 0,
            };
        }
        return target.accounts[label];
    }
    scheduleFlush() {
        if (!this.filePath || this.flushTimer) {
            return;
        }
        this.flushTimer = setTimeout(() => {
            this.flushTimer = null;
            void this.flush().catch((error) => {
                this.lastError = this.describeError(error);
                this.scheduleFlush();
            });
        }, this.flushIntervalMs);
        this.flushTimer.unref?.();
    }
    cancelFlushTimer() {
        if (this.flushTimer) {
            clearTimeout(this.flushTimer);
            this.flushTimer = null;
        }
    }
    async readSnapshot() {
        if (!this.filePath) {
            return null;
        }
        let raw;
        try {
            raw = await readFile(this.filePath, "utf8");
        }
        catch (error) {
            if (isMissingFileError(error)) {
                return null;
            }
            throw error;
        }
        const parsed = JSON.parse(raw);
        if (!validSnapshot(parsed)) {
            throw new InvalidProxyStatsSnapshotError(`Invalid proxy stats snapshot ${this.filePath}`);
        }
        return parsed;
    }
    async readTerminalErrorSnapshot() {
        if (!this.terminalErrorsFilePath) {
            return null;
        }
        let raw;
        try {
            raw = await readFile(this.terminalErrorsFilePath, "utf8");
        }
        catch (error) {
            if (isMissingFileError(error)) {
                return null;
            }
            throw error;
        }
        const parsed = JSON.parse(raw);
        if (!validTerminalErrorSnapshot(parsed)) {
            throw new InvalidProxyStatsSnapshotError(`Invalid proxy terminal-error snapshot ${this.terminalErrorsFilePath}`);
        }
        return parsed;
    }
    applySnapshot(snapshot) {
        this.stats = cloneStats(snapshot.stats);
        this.pending = emptyStats(this.now());
        this.revision = snapshot.revision;
        this.lastFlushedAt = snapshot.updatedAt;
    }
    applyTerminalErrorSnapshot(snapshot) {
        this.terminalErrors = cloneTerminalErrorJournal(snapshot.journal);
        this.pendingTerminalErrors = emptyTerminalErrorJournal(this.now());
        this.terminalErrorsRevision = snapshot.revision;
        this.terminalErrorsLastFlushedAt = snapshot.updatedAt;
    }
    async recoverCorruptSnapshot() {
        if (!this.filePath) {
            return null;
        }
        const releaseLock = await acquireFileLock(`${this.filePath}.lock`, this.lockTimeoutMs, this.staleLockMs, this.now);
        try {
            try {
                return await this.readSnapshot();
            }
            catch (error) {
                if (!isCorruptSnapshotError(error)) {
                    throw error;
                }
                await this.quarantineCorruptSnapshot();
                return null;
            }
        }
        finally {
            await releaseLock();
        }
    }
    async recoverCorruptTerminalErrorSnapshot() {
        if (!this.terminalErrorsFilePath) {
            return null;
        }
        const releaseLock = await acquireFileLock(`${this.terminalErrorsFilePath}.lock`, this.lockTimeoutMs, this.staleLockMs, this.now);
        try {
            try {
                return await this.readTerminalErrorSnapshot();
            }
            catch (error) {
                if (!isCorruptSnapshotError(error)) {
                    throw error;
                }
                await this.quarantineCorruptTerminalErrorSnapshot();
                return null;
            }
        }
        finally {
            await releaseLock();
        }
    }
    async quarantineCorruptSnapshot() {
        if (!this.filePath) {
            return;
        }
        const recoveredAt = this.now();
        const quarantinePath = `${this.filePath}.corrupt.${recoveredAt}.${randomUUID()}`;
        try {
            await rename(this.filePath, quarantinePath);
        }
        catch (error) {
            if (!isMissingFileError(error)) {
                throw error;
            }
            return;
        }
        this.lastRecoveryAt = recoveredAt;
        this.lastError = undefined;
        await this.pruneCorruptSnapshots(this.filePath);
    }
    async quarantineCorruptTerminalErrorSnapshot() {
        if (!this.terminalErrorsFilePath) {
            return;
        }
        const recoveredAt = this.now();
        const quarantinePath = `${this.terminalErrorsFilePath}.corrupt.${recoveredAt}.${randomUUID()}`;
        try {
            await rename(this.terminalErrorsFilePath, quarantinePath);
        }
        catch (error) {
            if (!isMissingFileError(error)) {
                throw error;
            }
            return;
        }
        this.terminalErrorsLastRecoveryAt = recoveredAt;
        this.terminalErrorsLastError = undefined;
        await this.pruneCorruptSnapshots(this.terminalErrorsFilePath);
    }
    async pruneCorruptSnapshots(filePath) {
        const directory = dirname(filePath);
        const prefix = `${basename(filePath)}.corrupt.`;
        const quarantined = (await readdir(directory))
            .filter((entry) => entry.startsWith(prefix))
            .sort()
            .reverse();
        await Promise.all(quarantined
            .slice(MAX_CORRUPT_SNAPSHOTS)
            .map((entry) => rm(join(directory, entry), { force: true })));
    }
    describeError(error) {
        return (error instanceof Error ? error.message : String(error)).slice(0, 1_000);
    }
}
const defaultStore = new ProxyUsageStatsStore();
export async function initUsageStats(filePath) {
    await defaultStore.initialize(filePath);
}
export function recordAttempt(accountLabel, accountType) {
    defaultStore.recordAttempt(accountLabel, accountType);
}
export function recordFinalSuccess(accountLabel, accountType) {
    defaultStore.recordFinalSuccess(accountLabel, accountType);
}
export function recordAttemptError(accountLabel, accountType, status, rateLimitKind) {
    defaultStore.recordAttemptError(accountLabel, accountType, status, rateLimitKind);
}
export function recordFinalError(status, accountLabel, accountType, details) {
    defaultStore.recordFinalError(status, accountLabel, accountType, details);
}
export function getStats() {
    return defaultStore.getStats();
}
export async function getReconciledStats() {
    return defaultStore.reconcile();
}
export async function getReconciledUsageSnapshot() {
    return defaultStore.reconcileUsageSnapshot();
}
export function getAccountStats(label) {
    return defaultStore.getAccountStats(label);
}
export function getTerminalErrors() {
    return defaultStore.getTerminalErrors();
}
export function getUsageStatsPersistenceStatus() {
    return defaultStore.getPersistenceStatus();
}
export async function flushUsageStats() {
    await defaultStore.flush();
}
/** Reset process-local counters while intentionally preserving durable state. */
export function resetStats() {
    defaultStore.resetMemory();
}
/** Disconnect persistence and clear singleton state between isolated tests. */
export async function resetUsageStatsForTests() {
    await defaultStore.resetForTests();
}
