import { APICallError } from "ai";
import { hasRetryableProperty } from "../error.js";
/**
 * Engine for making retry and fallback decisions.
 *
 * @example
 * ```typescript
 * const engine = new RetryEngine(config);
 *
 * // Check if we should retry
 * const decision = engine.getRetryDecision(error, session);
 *
 * if (decision.shouldRetry) {
 *   // Update provider if fallback
 *   if (decision.newProvider) {
 *     session.provider = decision.newProvider;
 *   }
 *   // Wait and retry
 *   await sleep(decision.delayMs);
 * } else {
 *   // Give up
 *   throw error;
 * }
 * ```
 */
export class RetryEngine {
    config;
    fallbackChain;
    fallbackIndex = 0;
    constructor(config) {
        this.config = config;
        this.fallbackChain = this.buildFallbackChain();
    }
    getMaxAttemptsForProvider(provider) {
        if (!this.config.fallback.enabled) {
            return this.config.retry.maxAttempts;
        }
        const { maxAttemptsPerProvider } = this.config.retry;
        if (typeof maxAttemptsPerProvider === "number") {
            return maxAttemptsPerProvider;
        }
        const attemptsForProvider = maxAttemptsPerProvider[provider.name];
        if (typeof attemptsForProvider === "number") {
            return attemptsForProvider;
        }
        return attemptsForProvider[provider.model] ?? 2;
    }
    getRetryDecision(error, session, abortSignal) {
        if (abortSignal?.aborted) {
            return { shouldRetry: false, delayMs: 0, reason: "aborted" };
        }
        if (!this.isRetryable(error)) {
            if (APICallError.isInstance(error)) {
                const statusCode = error.statusCode;
                let cause = "non-retryable";
                if (statusCode === 401 || statusCode === 403) {
                    cause = "auth_error";
                }
                if (statusCode === 404) {
                    cause = "not_found";
                }
                if (statusCode === 429) {
                    cause = "rate_limit";
                }
                if (statusCode === 500) {
                    cause = "api_error";
                }
                return { shouldRetry: false, delayMs: 0, reason: cause };
            }
            return { shouldRetry: false, delayMs: 0, reason: "non-retryable" };
        }
        // check time since last chunk, not total elapsed time. if chunks are flowing,
        // we're making progress and shouldn't timeout. only give up if we're stuck.
        const timeSinceActivity = session.getTimeSinceLastActivity();
        const { maxTotalTimeMs } = this.config.retry;
        if (timeSinceActivity >= maxTotalTimeMs) {
            return { shouldRetry: false, delayMs: 0, reason: "max-time" };
        }
        // exhausted attempts for current provider? try the fallback chain.
        // each provider gets maxAttempts tries before we move to the next one.
        const { maxAttempts } = this.config.retry;
        if (session.attempt >= maxAttempts) {
            // fallback chain exhausted, we're out of options
            return { shouldRetry: false, delayMs: 0, reason: "max-attempts" };
        }
        if (session.providerAttempt >=
            this.getMaxAttemptsForProvider(session.provider)) {
            const newProvider = this.advanceFallback(error);
            if (newProvider) {
                session.resetProviderAttempts();
                const delayMs = this.calculateDelay(session.attempt);
                return { shouldRetry: true, delayMs, newProvider };
            }
            // fallback chain exhausted, we're out of options
            return { shouldRetry: false, delayMs: 0, reason: "max-attempts" };
        }
        // still have attempts left, try again with the same provider
        const delayMs = this.calculateDelay(session.attempt);
        return { shouldRetry: true, delayMs };
    }
    getInitialProvider() {
        if (this.config.fallback.enabled && this.fallbackChain.length > 0) {
            const first = this.fallbackChain[0];
            return { name: first.provider, model: first.model };
        }
        return {
            name: this.config.provider.provider,
            model: this.config.provider.model,
        };
    }
    resetFallback() {
        this.fallbackIndex = 0;
    }
    isRetryable(error) {
        if (hasRetryableProperty(error)) {
            return error.isRetryable;
        }
        // Handle AI_RetryError wrapper from AI SDK - check the lastError's retryability
        if (this.isRetryErrorLike(error)) {
            if (hasRetryableProperty(error.lastError)) {
                return error.lastError.isRetryable;
            }
        }
        return true;
    }
    /**
     * Check if error is an AI_RetryError (wrapper error from AI SDK retry logic).
     */
    isRetryErrorLike(error) {
        return (typeof error === "object" &&
            error !== null &&
            "name" in error &&
            error.name === "AI_RetryError" &&
            "lastError" in error);
    }
    calculateDelay(attempt) {
        const { initialDelayMs, maxDelayMs, backoffMultiplier } = this.config.retry;
        const delay = initialDelayMs * Math.pow(backoffMultiplier, attempt - 1);
        return Math.min(delay, maxDelayMs);
    }
    advanceFallback(_error) {
        if (!this.config.fallback.enabled || this.fallbackChain.length === 0) {
            return undefined;
        }
        if (this.fallbackIndex >= this.fallbackChain.length - 1) {
            return undefined;
        }
        this.fallbackIndex++;
        const next = this.fallbackChain[this.fallbackIndex];
        return { name: next.provider, model: next.model };
    }
    /**
     * Build the fallback chain based on initial provider/model and strategy.
     *
     * The chain always starts with the initial provider/model from the config.
     * Strategy controls the fallback order:
     *
     * With retry (4+ entries):
     * - "model-then-provider": initial → initial+model2 → fallback providers+initial → fallback providers+model2
     * - "provider-then-model": initial → fallback providers+initial → initial+model2 → fallback providers+model2
     *
     * Without retry (3+ entries):
     * - "model-then-provider-no-retry": initial → initial+model2 → fallback providers+model2
     * - "provider-then-model-no-retry": initial → fallback providers+initial → fallback providers+model2
     *
     * Limited (2+ entries):
     * - "only-model": initial → initial+model2
     * - "only-provider": initial → fallback providers+initial
     *
     * No fallback (1 entry):
     * - "disabled": initial only
     */
    buildFallbackChain() {
        if (!this.config.fallback.enabled) {
            return [];
        }
        const { provider: initialProvider, model: initialModel } = this.config.provider;
        const fallbackProviders = this.config.fallback.fallbackProviders ??
            getDefaultFallbackProviders(initialProvider);
        const model2 = initialModel.startsWith("claude-opus")
            ? "claude-sonnet-4-6"
            : "claude-opus-4-6";
        const chain = [];
        const { strategy } = this.config.fallback;
        switch (strategy) {
            case "model-then-provider": {
                chain.push({ provider: initialProvider, model: initialModel });
                chain.push({ provider: initialProvider, model: model2 });
                for (const fp of fallbackProviders) {
                    chain.push({ provider: fp, model: initialModel });
                    chain.push({ provider: fp, model: model2 });
                }
                break;
            }
            case "provider-then-model": {
                chain.push({ provider: initialProvider, model: initialModel });
                for (const fp of fallbackProviders) {
                    chain.push({ provider: fp, model: initialModel });
                }
                chain.push({ provider: initialProvider, model: model2 });
                for (const fp of fallbackProviders) {
                    chain.push({ provider: fp, model: model2 });
                }
                break;
            }
            case "model-then-provider-no-retry": {
                chain.push({ provider: initialProvider, model: initialModel });
                chain.push({ provider: initialProvider, model: model2 });
                for (const fp of fallbackProviders) {
                    chain.push({ provider: fp, model: model2 });
                }
                break;
            }
            case "provider-then-model-no-retry": {
                chain.push({ provider: initialProvider, model: initialModel });
                for (const fp of fallbackProviders) {
                    chain.push({ provider: fp, model: initialModel });
                }
                for (const fp of fallbackProviders) {
                    chain.push({ provider: fp, model: model2 });
                }
                break;
            }
            case "only-model": {
                chain.push({ provider: initialProvider, model: initialModel });
                chain.push({ provider: initialProvider, model: model2 });
                break;
            }
            case "only-provider": {
                chain.push({ provider: initialProvider, model: initialModel });
                for (const fp of fallbackProviders) {
                    chain.push({ provider: fp, model: initialModel });
                }
                break;
            }
            case "disabled": {
                chain.push({ provider: initialProvider, model: initialModel });
                break;
            }
            default: {
                console.warn(`[retry-engine] Unknown fallback strategy: ${strategy}, defaulting to initial provider/model.`);
                chain.push({ provider: initialProvider, model: initialModel });
            }
        }
        return chain;
    }
}
function getDefaultFallbackProviders(initialProvider) {
    switch (initialProvider) {
        case "bedrock":
            return ["vertex"];
        case "vertex":
            return ["bedrock"];
        case "snowflake":
            return ["bedrock"];
    }
}
//# sourceMappingURL=retry-engine.js.map