/**
 * ProviderFallbackManager — tracks provider health and orchestrates
 * automatic fallback when a provider fails.
 *
 * When an LLM provider returns auth errors (401/402/403), accumulates
 * network failures, or exhausts stream silence retries, the manager
 * disables it with a cooldown and switches config.provider to the next
 * healthy provider in the server-provided priority list.
 *
 * Cooldowns:
 *   - Auth errors (401/402/403): 60 minutes
 *   - Network failures (3+ consecutive): 15 minutes
 *   - Stream silence exhausted: 15 minutes
 *
 * When all providers are exhausted, the manager notifies the owner,
 * sleeps 15 minutes, and resets all provider states.
 *
 * A server-side priority change (detected on heartbeat) resets all
 * states immediately so the new ordering takes effect.
 */
import { ProviderSchema, isProviderSupportedForModel } from "../config/types.js";
import { hasProviderCredentials } from "./model-provider.js";
import { logger } from "../logger.js";
const AUTH_COOLDOWN_MS = 60 * 60_000; // 60 minutes
const NETWORK_COOLDOWN_MS = 15 * 60_000; // 15 minutes
const SILENCE_COOLDOWN_MS = 15 * 60_000; // 15 minutes
const ALL_EXHAUSTED_SLEEP_MS = 15 * 60_000; // 15 minutes
const NETWORK_FAILURE_THRESHOLD = 3;
export class ProviderFallbackManager {
    config;
    priorityList = [];
    states = new Map();
    onSwitched = null;
    constructor(config) {
        this.config = config;
    }
    /**
     * Register a callback to run after every provider switch performed by
     * this manager. The callback fires only when `config.provider` actually
     * changed — both via `disableAndSwitch()` and via `resetAll()`.
     *
     * The most important consumer is `SessionManager.discardAllWarmQueries()`
     * — without it, a Claude warm subprocess can silently keep using the
     * previous provider for one more turn after fallback.
     */
    setOnProviderSwitched(cb) {
        this.onSwitched = cb;
    }
    notifySwitched(from, to) {
        if (from === to)
            return;
        const cb = this.onSwitched;
        if (!cb)
            return;
        try {
            const result = cb(from, to);
            if (result instanceof Promise) {
                result.catch((err) => {
                    logger.warn(`Provider-switch callback failed: ${err instanceof Error ? err.message : String(err)}`);
                });
            }
        }
        catch (err) {
            logger.warn(`Provider-switch callback threw synchronously: ${err instanceof Error ? err.message : String(err)}`);
        }
    }
    /**
     * Initialize or update the priority list from the server. If the list
     * changed, all provider states are reset so the new ordering takes
     * effect cleanly.
     */
    setPriorityList(serverPriority) {
        const parsed = [];
        for (const candidate of serverPriority) {
            const result = ProviderSchema.safeParse(candidate);
            if (!result.success)
                continue;
            const p = result.data;
            if (!isProviderSupportedForModel(this.config.model, p))
                continue;
            parsed.push(p);
        }
        if (parsed.length === 0)
            return;
        const changed = parsed.length !== this.priorityList.length ||
            parsed.some((p, i) => p !== this.priorityList[i]);
        if (changed) {
            if (this.priorityList.length > 0) {
                logger.system(`Provider priority changed: [${this.priorityList.join(", ")}] → [${parsed.join(", ")}] — resetting all provider states`);
            }
            this.priorityList = parsed;
            this.states.clear();
        }
    }
    get activeProvider() {
        return this.config.provider;
    }
    get disabledProviders() {
        const now = Date.now();
        const result = [];
        for (const [provider, state] of this.states) {
            if (state.disabledUntil && state.disabledUntil > now && state.disableReason) {
                result.push({
                    provider,
                    reason: state.disableReason,
                    disabledUntil: new Date(state.disabledUntil).toISOString(),
                });
            }
        }
        return result;
    }
    /**
     * Record a retryable network failure for the current provider.
     * Returns true if the threshold was reached and a provider switch
     * should be attempted.
     */
    recordNetworkFailure() {
        const state = this.getOrCreateState(this.config.provider);
        state.consecutiveNetworkFailures++;
        if (state.consecutiveNetworkFailures >= NETWORK_FAILURE_THRESHOLD) {
            return true;
        }
        return false;
    }
    /** Reset the network failure counter for the current provider (on success). */
    clearNetworkFailures() {
        const state = this.states.get(this.config.provider);
        if (state) {
            state.consecutiveNetworkFailures = 0;
        }
    }
    /**
     * Disable the current provider and attempt to switch to the next one.
     * Returns true if a switch was made, false if all providers are exhausted.
     */
    disableAndSwitch(reason) {
        const current = this.config.provider;
        const cooldownMs = reason === "auth" ? AUTH_COOLDOWN_MS
            : reason === "network" ? NETWORK_COOLDOWN_MS
                : SILENCE_COOLDOWN_MS;
        const state = this.getOrCreateState(current);
        state.disabledUntil = Date.now() + cooldownMs;
        state.disableReason = reason;
        state.consecutiveNetworkFailures = 0;
        const cooldownMin = cooldownMs / 60_000;
        logger.system(`Provider ${current} disabled for ${cooldownMin}min (reason: ${reason})`);
        const next = this.findNextProvider();
        if (!next) {
            logger.system("All providers exhausted — no healthy provider available");
            return false;
        }
        logger.system(`Provider fallback: ${current} → ${next}`);
        this.config.provider = next;
        this.notifySwitched(current, next);
        return true;
    }
    /**
     * Find the next healthy provider from the priority list.
     * A provider is healthy if it's not disabled (or cooldown has expired)
     * and has valid credentials.
     */
    findNextProvider() {
        const now = Date.now();
        for (const provider of this.priorityList) {
            if (provider === this.config.provider)
                continue;
            const state = this.states.get(provider);
            if (state?.disabledUntil && state.disabledUntil > now)
                continue;
            if (!hasProviderCredentials(provider, this.config))
                continue;
            return provider;
        }
        return null;
    }
    /**
     * Reset all provider states and switch to the first healthy provider.
     * Called after the all-exhausted sleep or when the server sends a new
     * priority list.
     */
    resetAll() {
        this.states.clear();
        // Switch to the first provider with valid credentials
        for (const provider of this.priorityList) {
            if (hasProviderCredentials(provider, this.config)) {
                if (provider !== this.config.provider) {
                    const previous = this.config.provider;
                    logger.system(`Provider reset: switching to ${provider}`);
                    this.config.provider = provider;
                    this.notifySwitched(previous, provider);
                }
                return;
            }
        }
    }
    /**
     * Sleep for the all-exhausted period, then reset.
     * The caller should await this and then retry.
     */
    async sleepAndReset() {
        const sleepMin = ALL_EXHAUSTED_SLEEP_MS / 60_000;
        logger.system(`All providers exhausted — sleeping ${sleepMin}min before resetting`);
        await new Promise((r) => setTimeout(r, ALL_EXHAUSTED_SLEEP_MS));
        this.resetAll();
    }
    /** Whether the fallback manager has a priority list configured. */
    get hasPriorityList() {
        return this.priorityList.length > 0;
    }
    getOrCreateState(provider) {
        let state = this.states.get(provider);
        if (!state) {
            state = {
                consecutiveNetworkFailures: 0,
                disabledUntil: null,
                disableReason: null,
            };
            this.states.set(provider, state);
        }
        return state;
    }
}
//# sourceMappingURL=provider-fallback.js.map