/**
 * Feature → model routing resolution.
 *
 * This is the authoritative runtime resolver: given the stored AI config and the
 * current model catalog, it decides which provider connection + model a feature
 * should use. Resolution order for each feature:
 *
 *   1. The feature's explicit `modelId` override (if it exists in the catalog).
 *   2. A capability-typed default that matches the feature's dominant required
 *      capability (e.g. a vision feature falls back to `defaultVisionModelId`).
 *   3. The global `defaultModelId`.
 *   4. Any `fallbackModelIds` listed on the feature.
 *
 * The first candidate that is present in the catalog, available, AND satisfies
 * every required capability wins. If none fully satisfy, we still return the
 * best candidate we found and report the missing capabilities so callers (UI +
 * enforcement) can warn rather than silently misroute. The admin UI mirrors a
 * simplified version of this for live preview, but THIS function is the source
 * of truth consumed by the AI runtime.
 */
import { modelsMatch } from './catalog.js';
/** Capability flags recognized on {@link AIModel.capabilities}. */
const CAPABILITY_KEYS = new Set([
    'text',
    'vision',
    'imageGeneration',
    'embeddings',
    'toolCalling',
    'structuredOutput',
    'streaming',
    'reasoning',
    'longContext',
]);
function findModel(catalog, id) {
    if (!id)
        return undefined;
    return catalog.find((m) => modelsMatch(m.id, id) || modelsMatch(m.providerModelId, id));
}
/** Required capabilities a model is missing. Unknown capability strings are ignored. */
function missingCaps(model, required) {
    const wanted = required.filter((c) => CAPABILITY_KEYS.has(c));
    if (!model)
        return wanted;
    const caps = model.capabilities;
    return wanted.filter((c) => !caps[c]);
}
/** Map a feature's dominant required capability to its typed default model id. */
function capabilityDefault(settings, required) {
    if (required.includes('imageGeneration'))
        return settings.defaultImageGenerationModelId;
    if (required.includes('embeddings'))
        return settings.defaultEmbeddingModelId;
    if (required.includes('vision'))
        return settings.defaultVisionModelId;
    if (required.includes('reasoning'))
        return settings.defaultReasoningModelId;
    if (required.includes('text'))
        return settings.defaultTextModelId;
    return undefined;
}
/**
 * Resolve a provider connection id for a model's provider key. Prefers an
 * explicit feature override, then the global default provider when it matches,
 * then the first enabled connection, then any connection of that provider.
 */
function providerConnId(providers, providerKey, preferredId) {
    if (preferredId) {
        const pref = providers.find((p) => p.id === preferredId && p.provider === providerKey);
        if (pref)
            return pref.id;
    }
    const enabled = providers.find((p) => p.provider === providerKey && p.enabled);
    if (enabled)
        return enabled.id;
    return providers.find((p) => p.provider === providerKey)?.id;
}
function build(featureKey, model, source, config, required, preferredProviderId) {
    const missing = missingCaps(model, required);
    return {
        featureKey,
        modelId: model.id,
        providerId: providerConnId(config.providers, model.provider, preferredProviderId ?? config.settings.defaultProviderId),
        model,
        source,
        satisfiesCapabilities: missing.length === 0 && model.status !== 'unavailable',
        missingCapabilities: missing,
    };
}
/** Resolve the provider + model a single feature should use. */
export function resolveFeatureModel(config, featureKey, catalog) {
    const { settings } = config;
    const feature = settings.features.find((f) => f.key === featureKey);
    const required = feature?.requiredCapabilities ?? [];
    const candidates = [
        { id: feature?.modelId, source: 'feature-override' },
        { id: capabilityDefault(settings, required), source: 'capability-default' },
        { id: settings.defaultModelId, source: 'global-default' },
        ...(feature?.fallbackModelIds ?? []).map((id) => ({ id, source: 'fallback' })),
    ];
    // Best case: first candidate that is catalogued, available, and capable.
    let firstFound;
    for (const candidate of candidates) {
        const model = findModel(catalog, candidate.id);
        if (!model)
            continue;
        if (!firstFound)
            firstFound = { model, source: candidate.source };
        if (model.status !== 'unavailable' && missingCaps(model, required).length === 0) {
            return build(featureKey, model, candidate.source, config, required, feature?.providerId);
        }
    }
    // A model was found but it's unavailable or capability-short — return it with a warning.
    if (firstFound) {
        return build(featureKey, firstFound.model, firstFound.source, config, required, feature?.providerId);
    }
    // An explicit override that isn't in the catalog yet (provider not synced).
    if (feature?.modelId) {
        return {
            featureKey,
            modelId: feature.modelId,
            providerId: feature.providerId ??
                providerConnId(config.providers, feature.modelId.split(':')[0] ?? 'custom', config.settings.defaultProviderId),
            source: 'feature-override',
            satisfiesCapabilities: false,
            missingCapabilities: required.filter((c) => CAPABILITY_KEYS.has(c)),
        };
    }
    return {
        featureKey,
        source: 'none',
        satisfiesCapabilities: required.filter((c) => CAPABILITY_KEYS.has(c)).length === 0,
        missingCapabilities: missingCaps(undefined, required),
    };
}
/** Resolve routing for every configured feature, keyed by feature key. */
export function resolveAllFeatureModels(config, catalog) {
    const out = {};
    for (const feature of config.settings.features) {
        out[feature.key] = resolveFeatureModel(config, feature.key, catalog);
    }
    return out;
}
//# sourceMappingURL=resolve.js.map