/**
 * Provider-owned request-retry policy configuration and resolution.
 *
 * Adapters expose one resolved policy per registered provider route; the
 * optional dsh-llm-retry plugin executes it on the agent's failed-step extension point.
 *
 * @module @deepseek-ai/dsh-llm/retry-policy
 */
import z from '@deepseek-ai/schemastery';
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout';
import { EMPTY_RESPONSE_CODE } from "./error.js";
const DEFAULT_MAX_RETRIES = 5;
const DEFAULT_INITIAL_DELAY_MS = 500;
const DEFAULT_MAX_DELAY_MS = 10_000;
const DEFAULT_JITTER_RATIO = 0.1;
const DEFAULT_RETRYABLE_CODES = Object.freeze([
    EMPTY_RESPONSE_CODE,
    'RATE_LIMIT',
    'SERVER',
    'TIMEOUT',
    'TRANSPORT',
]);
const backoffSchema = z.object({
    initialDelayMs: z.number().max(MAX_TIMER_DELAY_MS).default(DEFAULT_INITIAL_DELAY_MS),
    maxDelayMs: z.number().max(MAX_TIMER_DELAY_MS).default(DEFAULT_MAX_DELAY_MS),
    jitterRatio: z.number().min(0).max(1).default(DEFAULT_JITTER_RATIO),
});
const normalPolicySchema = z.object({
    mode: z.const('normal').required(),
    maxRetries: z.number().step(1).min(0).max(Number.MAX_SAFE_INTEGER).default(DEFAULT_MAX_RETRIES),
    retryableCodes: z.array(z.string()).default([...DEFAULT_RETRYABLE_CODES]),
    backoff: backoffSchema,
});
const alwaysPolicySchema = z.object({
    mode: z.const('always').required(),
    backoff: backoffSchema,
});
/** Cordis schema embedded by each concrete provider configuration. */
export const RetryPolicySchema = z.union([
    normalPolicySchema,
    alwaysPolicySchema,
]);
const NORMAL_POLICY_KEYS = new Set([
    'mode', 'maxRetries', 'retryableCodes', 'backoff',
]);
// Layered configuration can retain normal-only fields after switching modes;
// always mode ignores those inactive values while still rejecting unknown keys.
const ALWAYS_POLICY_KEYS = new Set([
    'mode', 'maxRetries', 'retryableCodes', 'backoff',
]);
const BACKOFF_KEYS = new Set(['initialDelayMs', 'maxDelayMs', 'jitterRatio']);
function validateKeys(value, allowed, path) {
    for (const key of Object.keys(value)) {
        if (!allowed.has(key))
            throw new Error(`${path}: unknown key "${key}"`);
    }
}
function resolveBackoff(config, path) {
    if (config !== undefined)
        validateKeys(config, BACKOFF_KEYS, path);
    const initialDelayMs = config?.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS;
    const maxDelayMs = config?.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
    const jitterRatio = config?.jitterRatio ?? DEFAULT_JITTER_RATIO;
    if (!Number.isFinite(initialDelayMs) || initialDelayMs <= 0 || initialDelayMs > MAX_TIMER_DELAY_MS) {
        throw new Error(`${path}.initialDelayMs must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`);
    }
    if (!Number.isFinite(maxDelayMs) || maxDelayMs <= 0 || maxDelayMs > MAX_TIMER_DELAY_MS) {
        throw new Error(`${path}.maxDelayMs must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`);
    }
    if (initialDelayMs > maxDelayMs) {
        throw new Error(`${path}.initialDelayMs must be less than or equal to maxDelayMs`);
    }
    if (!Number.isFinite(jitterRatio) || jitterRatio < 0 || jitterRatio > 1) {
        throw new Error(`${path}.jitterRatio must be between 0 and 1`);
    }
    return Object.freeze({ initialDelayMs, maxDelayMs, jitterRatio });
}
/**
 * Validate, default, and detach one provider-owned retry policy.
 * @param config - optional provider configuration; omission selects normal defaults.
 * @param path - diagnostic path naming the provider config that owns the value.
 * @returns an immutable policy safe to capture in provider registration state.
 */
export function resolveRetryPolicy(config, path) {
    if (config === undefined) {
        return Object.freeze({
            mode: 'normal',
            maxRetries: DEFAULT_MAX_RETRIES,
            retryableCodes: DEFAULT_RETRYABLE_CODES,
            ...resolveBackoff(undefined, `${path}.backoff`),
        });
    }
    switch (config.mode) {
        case 'normal': {
            validateKeys(config, NORMAL_POLICY_KEYS, path);
            const maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
            const retryableCodes = config.retryableCodes ?? [...DEFAULT_RETRYABLE_CODES];
            if (!Number.isSafeInteger(maxRetries) || maxRetries < 0) {
                throw new Error(`${path}.maxRetries must be a non-negative safe integer`);
            }
            if (retryableCodes.length === 0) {
                throw new Error(`${path}.retryableCodes must not be empty`);
            }
            if (retryableCodes.some(code => typeof code !== 'string' || code.length === 0)) {
                throw new Error(`${path}.retryableCodes must contain only non-empty strings`);
            }
            if (new Set(retryableCodes).size !== retryableCodes.length) {
                throw new Error(`${path}.retryableCodes must not contain duplicates`);
            }
            return Object.freeze({
                mode: 'normal',
                maxRetries,
                retryableCodes: Object.freeze([...retryableCodes]),
                ...resolveBackoff(config.backoff, `${path}.backoff`),
            });
        }
        case 'always':
            validateKeys(config, ALWAYS_POLICY_KEYS, path);
            return Object.freeze({
                mode: 'always',
                ...resolveBackoff(config.backoff, `${path}.backoff`),
            });
        default:
            throw new Error(`${path}.mode must be "normal" or "always"`);
    }
}
//# sourceMappingURL=retry-policy.js.map