/**
 * Provider Optimizer
 *
 * Lives above provider implementations. Routes requests based on capability
 * contracts from ProviderCapabilityRegistry. Supports auto/manual/pinned
 * routing modes with deterministic, fully-explainable decisions.
 *
 * Optimizer off → zero behavior change (selectRoute returns null).
 * Optimizer on  → deterministic route explanation for every request profile.
 */
import { ProviderCapabilityRegistry, } from './capabilities.js';
// ---------------------------------------------------------------------------
// ProviderOptimizer
// ---------------------------------------------------------------------------
/**
 * Optimizer that selects the best provider/model for a given request profile.
 *
 * When disabled (`enabled = false`) every method returns null/empty — the
 * optimizer has zero effect on normal request flow.
 *
 * When enabled, routing decisions are driven entirely by `ProviderCapabilityRegistry`
 * capability contracts. The selection algorithm is deterministic: candidates are
 * evaluated in registry order (custom → synthetic → catalog → discovered), and
 * the first capable model wins. Ties are never broken by opaque scoring.
 */
export class ProviderOptimizer {
    _mode = 'manual';
    _enabled;
    _pinnedProvider = null;
    _pinnedModel = null;
    _fallbackLog = [];
    static MAX_LOG_ENTRIES = 200;
    _clock;
    registry;
    capabilityRegistry;
    constructor(registry, capabilityRegistry, enabled = false, clock = Date.now) {
        this.registry = registry;
        this.capabilityRegistry = capabilityRegistry;
        this._enabled = enabled;
        this._clock = clock;
    }
    // -------------------------------------------------------------------------
    // Mode control
    // -------------------------------------------------------------------------
    /** Current routing mode. */
    get mode() {
        return this._mode;
    }
    /** Whether the optimizer is active. When false, selectRoute always returns null. */
    get enabled() {
        return this._enabled;
    }
    /** Enable or disable the optimizer. */
    setEnabled(enabled) {
        this._enabled = enabled;
    }
    /**
     * Set the routing mode.
     * - `auto`   — optimizer selects the best capable provider for each request profile.
     * - `manual` — optimizer is advisory only; caller drives provider selection.
     * - `pinned` — optimizer always returns the pinned provider/model (if capable).
     */
    setMode(mode) {
        this._mode = mode;
        if (mode !== 'pinned') {
            this._pinnedProvider = null;
            this._pinnedModel = null;
        }
    }
    // -------------------------------------------------------------------------
    // Pinning
    // -------------------------------------------------------------------------
    /**
     * Pin routing to a specific provider and model.
     * Automatically switches mode to `pinned`.
     *
     * @param providerId - Provider name (e.g. `'anthropic'`).
     * @param modelId    - Model ID (e.g. `'claude-opus-4-5'`).
     */
    pin(providerId, modelId) {
        this._pinnedProvider = providerId;
        this._pinnedModel = modelId;
        this._mode = 'pinned';
    }
    /** Remove the current pin and return to `manual` mode. */
    unpin() {
        this._pinnedProvider = null;
        this._pinnedModel = null;
        this._mode = 'manual';
    }
    /** Current pin target, or null if not pinned. */
    get pinnedTarget() {
        if (this._pinnedProvider && this._pinnedModel) {
            return { providerId: this._pinnedProvider, modelId: this._pinnedModel };
        }
        return null;
    }
    // -------------------------------------------------------------------------
    // Route selection
    // -------------------------------------------------------------------------
    /**
     * Select the best route for the given request profile.
     *
     * Returns `null` when the optimizer is disabled — callers must handle null
     * and fall through to their own provider selection logic.
     *
     * @param profile        - Capability requirements for the request.
     * @param healthSnapshot - Optional map of provider health records for filtering
     *                         unhealthy providers in `auto` mode.
     * @returns A `RouteDecision` or `null` when optimizer is off.
     *
     * @remarks
     * `selectRoute` is wired by the orchestrator when the `provider-optimizer`
     * feature flag is enabled. This follows the same deferred-integration pattern
     * as session emitters — the method is fully functional but called externally
     * only when the feature is active. Until then it is a no-op (returns `null`).
     */
    selectRoute(profile, healthSnapshot) {
        if (!this._enabled)
            return null;
        const candidates = this.registry.getSelectableModels();
        const allCandidates = [];
        // Pinned mode — evaluate only the pinned target
        if (this._mode === 'pinned' && this._pinnedProvider && this._pinnedModel) {
            const explanation = this.capabilityRegistry.getRouteExplanation(this._pinnedProvider, this._pinnedModel, profile);
            allCandidates.push(explanation);
            return {
                providerId: this._pinnedProvider,
                modelId: this._pinnedModel,
                explanation,
                allCandidates,
                decidedAt: this._clock(),
                pinned: true,
            };
        }
        // Auto mode — evaluate all selectable models; pick first capable
        // Manual mode — same evaluation but result is advisory (caller may ignore)
        let selected = null;
        let selectedExplanation = null;
        for (const model of candidates) {
            // Skip unhealthy providers in auto mode when health data is available
            if (this._mode === 'auto' && healthSnapshot) {
                const health = healthSnapshot.get(model.provider);
                if (health && (health.status === 'unavailable' || health.status === 'auth_error')) {
                    continue;
                }
            }
            const explanation = this.capabilityRegistry.getRouteExplanation(model.provider, model.id, profile);
            allCandidates.push(explanation);
            if (explanation.accepted && !selected) {
                selected = model;
                selectedExplanation = explanation;
            }
        }
        if (!selected || !selectedExplanation) {
            // No capable provider found — return first candidate's explanation as the decision
            // so callers get a fully-populated rejection explanation
            const fallbackExpl = allCandidates[0] ?? this._emptyExplanation();
            return {
                providerId: fallbackExpl.providerId,
                modelId: fallbackExpl.modelId,
                explanation: fallbackExpl,
                allCandidates,
                decidedAt: this._clock(),
                pinned: false,
            };
        }
        return {
            providerId: selected.provider,
            modelId: selected.id,
            explanation: selectedExplanation,
            allCandidates,
            decidedAt: this._clock(),
            pinned: false,
        };
    }
    /**
     * Explain the current routing decision for the active model without changing it.
     * Always returns a full explanation even when the optimizer is in manual mode.
     *
     * @param profile - Optional request profile; defaults to empty (no requirements).
     */
    explainCurrentRoute(profile = {}) {
        const current = this.registry.getCurrentModel();
        return this.registry.explainRoute(current.registryKey, profile);
    }
    // -------------------------------------------------------------------------
    // Private helpers
    // -------------------------------------------------------------------------
    /** Returns a fully-typed rejection explanation when no candidates are available. */
    _emptyExplanation() {
        return {
            accepted: false,
            providerId: 'none',
            modelId: 'none',
            summary: 'No selectable models available in registry.',
            rejections: [],
            capability: this.capabilityRegistry.getCapability('unknown', 'unknown'),
        };
    }
    // -------------------------------------------------------------------------
    // Fallback test
    // -------------------------------------------------------------------------
    /**
     * Simulate the fallback chain by evaluating all selectable models against
     * the given profile. Returns an ordered list of results (capable first,
     * then incapable) so operators can visualize the full fallback topology.
     *
     * @param profile - Capability requirements to test against.
     */
    testFallback(profile = {}) {
        const candidates = this.registry.getSelectableModels();
        const chain = [];
        let position = 0;
        for (const model of candidates) {
            const explanation = this.capabilityRegistry.getRouteExplanation(model.provider, model.id, profile);
            chain.push({
                position,
                providerId: model.provider,
                modelId: model.id,
                capable: explanation.accepted,
                explanation,
            });
            position++;
        }
        const viableCount = chain.filter((n) => n.capable).length;
        return {
            chain,
            viableCount,
            totalCount: chain.length,
            testedAt: this._clock(),
        };
    }
    // -------------------------------------------------------------------------
    // Fallback transition log
    // -------------------------------------------------------------------------
    /**
     * Record a fallback transition. Called by orchestration layers when a
     * provider fails and routing switches to a fallback.
     *
     * Transitions are always recorded regardless of optimizer enabled state
     * so that manual fallbacks are also visible in the log.
     */
    recordFallbackTransition(from, to, reason) {
        const entry = {
            ts: this._clock(),
            from,
            to,
            reason,
        };
        this._fallbackLog.push(entry);
        // Trim to bounded size
        if (this._fallbackLog.length > ProviderOptimizer.MAX_LOG_ENTRIES) {
            this._fallbackLog.splice(0, this._fallbackLog.length - ProviderOptimizer.MAX_LOG_ENTRIES);
        }
    }
    /** All recorded fallback transitions (oldest first). */
    get fallbackLog() {
        return this._fallbackLog;
    }
    /** Clear the fallback transition log. */
    clearFallbackLog() {
        this._fallbackLog.splice(0);
    }
}
