import { computeNextDailyResetAtMs } from './apikey-reset.js';
import { applyErrorEvent, applySuccessEvent, createInitialQuotaState, tickQuotaStateTime } from './quota-state.js';
function safeTrim(value) {
    return typeof value === 'string' ? value.trim() : '';
}
function readEnvDurationMs(envKey, fallbackMs) {
    const raw = String(process.env[envKey] ?? '').trim();
    if (!raw)
        return fallbackMs;
    const n = Number(raw);
    if (!Number.isFinite(n) || n <= 0)
        return fallbackMs;
    return Math.floor(n);
}
const ANTIGRAVITY_AUTH_VERIFY_BAN_MS = readEnvDurationMs('ROUTECODEX_ANTIGRAVITY_AUTH_VERIFY_BAN', 24 * 60 * 60_000);
const ANTIGRAVITY_THOUGHT_SIGNATURE_MISSING_COOLDOWN_MS = readEnvDurationMs('ROUTECODEX_ANTIGRAVITY_THOUGHT_SIGNATURE_MISSING_COOLDOWN', 5 * 60_000);
function parseProviderKey(ev) {
    const key = safeTrim(ev?.providerKey);
    return key;
}
function parseHttpStatus(ev) {
    const status = ev?.status;
    if (typeof status === 'number' && Number.isFinite(status)) {
        return status;
    }
    const runtime = ev?.runtime;
    const maybe = runtime && typeof runtime === 'object' ? runtime.status : null;
    if (typeof maybe === 'number' && Number.isFinite(maybe)) {
        return maybe;
    }
    return null;
}
function extractResetAtIso(details) {
    if (!details || typeof details !== 'object') {
        return null;
    }
    const v = details.resetAt;
    const raw = typeof v === 'string' ? v.trim() : '';
    if (raw) {
        return raw;
    }
    const meta = details.meta && typeof details.meta === 'object' && !Array.isArray(details.meta)
        ? details.meta
        : null;
    const metaResetAt = meta && typeof meta.resetAt === 'string' ? String(meta.resetAt).trim() : '';
    if (metaResetAt) {
        return metaResetAt;
    }
    return null;
}
function extractResetAtIsoFromText(text) {
    const raw = typeof text === 'string' ? text.trim() : '';
    if (!raw) {
        return null;
    }
    // Try to parse an embedded JSON payload (common in provider error messages).
    const firstBrace = raw.indexOf('{');
    const lastBrace = raw.lastIndexOf('}');
    if (firstBrace >= 0 && lastBrace > firstBrace) {
        const jsonCandidate = raw.slice(firstBrace, lastBrace + 1);
        try {
            const parsed = JSON.parse(jsonCandidate);
            const direct = parsed && typeof parsed.resetAt === 'string' ? String(parsed.resetAt).trim() : '';
            if (direct) {
                return direct;
            }
            const nested = parsed && parsed.error && typeof parsed.error === 'object' ? parsed.error : null;
            const nestedResetAt = nested && typeof nested.resetAt === 'string' ? String(nested.resetAt).trim() : '';
            if (nestedResetAt) {
                return nestedResetAt;
            }
        }
        catch {
            // ignore JSON parse failures; fall back to regex matching
        }
    }
    const m = raw.match(/"resetAt"\s*:\s*"([^"]+)"/i) ||
        raw.match(/"reset_at"\s*:\s*"([^"]+)"/i) ||
        raw.match(/resetAt\s*[:=]\s*"?([0-9]{4}-[0-9]{2}-[0-9]{2}T[^"\s]+)"?/i) ||
        raw.match(/reset_at\s*[:=]\s*"?([0-9]{4}-[0-9]{2}-[0-9]{2}T[^"\s]+)"?/i);
    const captured = m && typeof m[1] === 'string' ? m[1].trim() : '';
    return captured ? captured : null;
}
function extractResetAtIsoFromSources(sources) {
    for (const source of sources) {
        const candidate = extractResetAtIsoFromText(source);
        if (candidate) {
            return candidate;
        }
    }
    return null;
}
function collectUpstreamErrorSources(ev) {
    const sources = [];
    const msg = typeof ev.message === 'string' ? ev.message : '';
    if (msg)
        sources.push(msg);
    const details = ev.details && typeof ev.details === 'object' && !Array.isArray(ev.details) ? ev.details : null;
    if (details) {
        const upstreamMessage = typeof details.upstreamMessage === 'string' ? details.upstreamMessage : '';
        if (upstreamMessage && upstreamMessage.trim())
            sources.push(upstreamMessage);
        const meta = details.meta && typeof details.meta === 'object' && !Array.isArray(details.meta) ? details.meta : null;
        if (meta) {
            const metaUpstream = typeof meta.upstreamMessage === 'string' ? meta.upstreamMessage : '';
            const metaMessage = typeof meta.message === 'string' ? meta.message : '';
            if (metaUpstream && metaUpstream.trim())
                sources.push(metaUpstream);
            if (metaMessage && metaMessage.trim())
                sources.push(metaMessage);
        }
    }
    return sources;
}
function isAntigravityProviderKey(providerKey) {
    return providerKey.toLowerCase().startsWith('antigravity.');
}
function isGoogleAccountVerificationRequired(ev, sources) {
    if (!sources.length) {
        sources = collectUpstreamErrorSources(ev);
    }
    const lowered = sources.join(' | ').toLowerCase();
    return (lowered.includes('verify your account') ||
        lowered.includes('validation_required') ||
        lowered.includes('validation required') ||
        lowered.includes('validation_url') ||
        lowered.includes('validation url') ||
        lowered.includes('accounts.google.com/signin/continue') ||
        lowered.includes('support.google.com/accounts?p=al_alert'));
}
function isThoughtSignatureMissing(ev, sources) {
    const code = typeof ev.code === 'string' ? ev.code.trim().toLowerCase() : '';
    if (code.includes('thought') && code.includes('signature')) {
        return true;
    }
    if (!sources.length) {
        sources = collectUpstreamErrorSources(ev);
    }
    for (const source of sources) {
        const lowered = source.toLowerCase();
        const mentionsSignature = lowered.includes('thoughtsignature') ||
            lowered.includes('thought signature') ||
            lowered.includes('reasoning_signature') ||
            lowered.includes('reasoning signature');
        if (!mentionsSignature)
            continue;
        if (lowered.includes('missing') ||
            lowered.includes('required') ||
            lowered.includes('invalid') ||
            lowered.includes('not provided') ||
            lowered.includes('签名') ||
            lowered.includes('缺少') ||
            lowered.includes('无效')) {
            return true;
        }
    }
    return false;
}
function extractFirstUrl(sources) {
    for (const source of sources) {
        const m = String(source || '').match(/https?:\/\/\S+/);
        if (m && m[0]) {
            const url = m[0].replace(/[)\],.]+$/g, '');
            return url || null;
        }
    }
    return null;
}
export class QuotaManager {
    staticConfigs = new Map();
    states = new Map();
    store;
    persistTimer = null;
    dirty = false;
    constructor(options) {
        this.store = options?.store ?? null;
    }
    async hydrateFromStore() {
        if (!this.store) {
            return;
        }
        const snapshot = await this.store.load().catch(() => null);
        if (!snapshot || !snapshot.providers || typeof snapshot.providers !== 'object') {
            return;
        }
        const nowMs = Date.now();
        for (const [providerKey, raw] of Object.entries(snapshot.providers)) {
            if (!raw || typeof raw !== 'object')
                continue;
            const key = safeTrim(providerKey) || safeTrim(raw.providerKey);
            if (!key)
                continue;
            const state = raw;
            this.states.set(key, tickQuotaStateTime({ ...state, providerKey: key }, nowMs));
        }
    }
    registerProviderStaticConfig(providerKey, cfg) {
        const key = safeTrim(providerKey);
        if (!key)
            return;
        this.staticConfigs.set(key, { ...cfg });
        if (!this.states.has(key)) {
            this.states.set(key, createInitialQuotaState(key, cfg, Date.now()));
            this.markDirty();
        }
    }
    ensureProvider(providerKey) {
        const key = safeTrim(providerKey);
        if (!key) {
            throw new Error('providerKey is required');
        }
        const existing = this.states.get(key);
        if (existing) {
            return existing;
        }
        const cfg = this.staticConfigs.get(key);
        const seeded = createInitialQuotaState(key, cfg, Date.now());
        this.states.set(key, seeded);
        this.markDirty();
        return seeded;
    }
    disableProvider(options) {
        const key = safeTrim(options.providerKey);
        if (!key)
            return;
        const nowMs = Date.now();
        const state = this.ensureProvider(key);
        const until = nowMs + Math.max(0, Math.floor(options.durationMs));
        const next = options.mode === 'blacklist'
            ? { ...state, blacklistUntil: Math.max(state.blacklistUntil ?? 0, until) || until, reason: 'blacklist', inPool: false }
            : { ...state, cooldownUntil: Math.max(state.cooldownUntil ?? 0, until) || until, reason: 'cooldown', inPool: false };
        this.states.set(key, tickQuotaStateTime(next, nowMs));
        this.markDirty();
    }
    recoverProvider(providerKey) {
        const key = safeTrim(providerKey);
        if (!key)
            return;
        const nowMs = Date.now();
        const state = this.ensureProvider(key);
        const next = {
            ...state,
            cooldownUntil: null,
            blacklistUntil: null,
            authIssue: null,
            reason: 'ok',
            inPool: true
        };
        this.states.set(key, tickQuotaStateTime(next, nowMs));
        this.markDirty();
    }
    resetProvider(providerKey) {
        const key = safeTrim(providerKey);
        if (!key)
            return;
        const nowMs = Date.now();
        const cfg = this.staticConfigs.get(key);
        this.states.set(key, createInitialQuotaState(key, cfg, nowMs));
        this.markDirty();
    }
    onProviderError(ev) {
        const providerKey = parseProviderKey(ev.runtime) || safeTrim(ev?.runtime?.providerKey) || safeTrim(ev?.providerKey);
        if (!providerKey)
            return;
        const nowMs = typeof ev.timestamp === 'number' && Number.isFinite(ev.timestamp) ? ev.timestamp : Date.now();
        const status = parseHttpStatus(ev);
        const code = safeTrim(ev.code) || safeTrim(ev?.runtime?.errorCode);
        const details = ev.details && typeof ev.details === 'object' ? ev.details : undefined;
        const state = this.ensureProvider(providerKey);
        // Antigravity account-scope auth verification required: isolate to current providerKey only.
        if (isAntigravityProviderKey(providerKey)) {
            const sources = collectUpstreamErrorSources(ev);
            if (isGoogleAccountVerificationRequired(ev, sources)) {
                const url = extractFirstUrl(sources);
                const banUntil = nowMs + ANTIGRAVITY_AUTH_VERIFY_BAN_MS;
                const next = {
                    ...state,
                    inPool: false,
                    reason: 'authVerify',
                    authIssue: {
                        kind: 'google_account_verification',
                        ...(url ? { url } : {}),
                        message: 'account verification required'
                    },
                    blacklistUntil: Math.max(state.blacklistUntil ?? 0, banUntil) || banUntil,
                    cooldownUntil: null,
                    lastErrorAtMs: nowMs,
                    lastErrorCode: code || 'AUTH_VERIFY',
                    lastErrorSeries: 'EFATAL',
                    consecutiveErrorCount: 0
                };
                this.states.set(providerKey, tickQuotaStateTime(next, nowMs));
                this.markDirty();
                return;
            }
            // Thought signature missing: cooldown current providerKey only.
            if (isThoughtSignatureMissing(ev, sources)) {
                const freezeUntil = nowMs + ANTIGRAVITY_THOUGHT_SIGNATURE_MISSING_COOLDOWN_MS;
                const next = {
                    ...state,
                    inPool: false,
                    reason: 'cooldown',
                    cooldownUntil: Math.max(state.cooldownUntil ?? 0, freezeUntil) || freezeUntil,
                    lastErrorAtMs: nowMs,
                    lastErrorCode: code || 'SIGNATURE_MISSING',
                    lastErrorSeries: 'EOTHER',
                    consecutiveErrorCount: 0
                };
                this.states.set(providerKey, tickQuotaStateTime(next, nowMs));
                this.markDirty();
                return;
            }
        }
        // HTTP 402: treat as quota depleted -> blacklist until reset.
        if (status === 402 || code.toUpperCase() === 'HTTP_402') {
            const resetAtIso = extractResetAtIso(details) ??
                extractResetAtIsoFromSources(collectUpstreamErrorSources(ev));
            let resetAtMs = null;
            if (resetAtIso) {
                const parsed = Date.parse(resetAtIso);
                if (Number.isFinite(parsed) && parsed > nowMs) {
                    resetAtMs = parsed;
                }
            }
            if (resetAtMs === null) {
                const staticCfg = this.staticConfigs.get(providerKey);
                const configuredResetTime = staticCfg && typeof staticCfg.apikeyDailyResetTime === 'string'
                    ? String(staticCfg.apikeyDailyResetTime).trim()
                    : null;
                resetAtMs = computeNextDailyResetAtMs({ nowMs, resetTime: configuredResetTime }).resetAtMs;
            }
            const next = {
                ...state,
                inPool: false,
                reason: 'blacklist',
                blacklistUntil: Math.max(state.blacklistUntil ?? 0, resetAtMs) || resetAtMs,
                lastErrorAtMs: nowMs,
                lastErrorCode: code || 'HTTP_402',
                lastErrorSeries: 'EOTHER',
                consecutiveErrorCount: 0
            };
            this.states.set(providerKey, tickQuotaStateTime(next, nowMs));
            this.markDirty();
            return;
        }
        const authIssue = details?.authIssue && typeof details.authIssue === 'object'
            ? details.authIssue
            : null;
        const mapped = {
            providerKey,
            code: code || null,
            httpStatus: typeof status === 'number' ? status : null,
            fatal: Boolean(ev.recoverable === false || ev.affectsHealth === false ? false : ev?.fatal) || null,
            timestampMs: nowMs,
            resetAt: extractResetAtIso(details),
            authIssue: authIssue || null
        };
        const next = applyErrorEvent(state, mapped, nowMs);
        this.states.set(providerKey, tickQuotaStateTime(next, nowMs));
        this.markDirty();
    }
    onProviderSuccess(ev) {
        const providerKey = safeTrim(ev.runtime?.providerKey);
        if (!providerKey)
            return;
        const nowMs = typeof ev.timestamp === 'number' && Number.isFinite(ev.timestamp) ? ev.timestamp : Date.now();
        const state = this.ensureProvider(providerKey);
        const mapped = { providerKey, timestampMs: nowMs };
        const next = applySuccessEvent(state, mapped, nowMs);
        this.states.set(providerKey, next);
        this.markDirty();
    }
    /**
     * External quota snapshot ingestion hook (host-driven).
     * This API is intentionally small: the host adapter translates provider-specific
     * quota responses into a normalized per-providerKey inPool/cooldown/blacklist update.
     */
    updateProviderPoolState(options) {
        const key = safeTrim(options.providerKey);
        if (!key)
            return;
        const nowMs = Date.now();
        const state = this.ensureProvider(key);
        const next = {
            ...state,
            inPool: Boolean(options.inPool),
            reason: options.inPool ? 'ok' : state.reason,
            cooldownUntil: typeof options.cooldownUntil === 'number' ? options.cooldownUntil : state.cooldownUntil,
            blacklistUntil: typeof options.blacklistUntil === 'number' ? options.blacklistUntil : state.blacklistUntil,
            ...(typeof options.reason === 'string' && options.reason.trim()
                ? { reason: options.reason.trim() }
                : {})
        };
        this.states.set(key, tickQuotaStateTime(next, nowMs));
        this.markDirty();
    }
    getQuotaView() {
        return (providerKey) => {
            const key = safeTrim(providerKey);
            if (!key)
                return null;
            const nowMs = Date.now();
            const state = this.states.get(key);
            if (!state) {
                return null;
            }
            const normalized = tickQuotaStateTime(state, nowMs);
            if (normalized !== state) {
                this.states.set(key, normalized);
            }
            const withinBlacklist = typeof normalized.blacklistUntil === 'number' && normalized.blacklistUntil > nowMs;
            const withinCooldown = typeof normalized.cooldownUntil === 'number' && normalized.cooldownUntil > nowMs;
            const inPool = normalized.inPool && !withinBlacklist && !withinCooldown;
            const hasRecentError = typeof normalized.lastErrorAtMs === 'number' &&
                Number.isFinite(normalized.lastErrorAtMs) &&
                nowMs - normalized.lastErrorAtMs >= 0 &&
                nowMs - normalized.lastErrorAtMs <= 30_000;
            const selectionPenalty = hasRecentError && typeof normalized.consecutiveErrorCount === 'number' && normalized.consecutiveErrorCount > 0
                ? Math.max(0, Math.floor(normalized.consecutiveErrorCount))
                : 0;
            return {
                providerKey: key,
                inPool,
                reason: normalized.reason,
                priorityTier: normalized.priorityTier,
                selectionPenalty,
                lastErrorAtMs: normalized.lastErrorAtMs,
                consecutiveErrorCount: normalized.consecutiveErrorCount,
                cooldownUntil: normalized.cooldownUntil,
                blacklistUntil: normalized.blacklistUntil
            };
        };
    }
    getSnapshot() {
        return {
            updatedAtMs: Date.now(),
            providers: Object.fromEntries(this.states.entries())
        };
    }
    markDirty() {
        this.dirty = true;
        if (this.persistTimer) {
            return;
        }
        // best-effort debounce
        this.persistTimer = setTimeout(() => {
            this.persistTimer = null;
            if (!this.dirty)
                return;
            this.dirty = false;
            void this.persistNow();
        }, 800);
    }
    async persistNow() {
        if (!this.store)
            return;
        const snapshot = this.getSnapshot();
        const payload = { savedAtMs: snapshot.updatedAtMs, providers: snapshot.providers };
        await this.store.save(payload).catch(() => {
            // persistence must never block routing
        });
    }
}
