/**
 * Model resolution, scoping, and initial selection
 */
import chalk from 'chalk';
import { DEFAULT_THINKING_LEVEL, isValidThinkingLevel } from '../../core/defaults.js';
import { SHORTCUT_MODEL_ID } from '../../model-ids.js';
export function formatModelRef(model) {
    return `${model.provider}/${model.id}`;
}
/**
 * Resolve an exact model reference.
 *
 * Accepts either a full provider/model-id reference or a bare model id. Bare ids
 * must be unambiguous. Model ids may themselves contain slashes.
 */
export function resolveExactModelRef(availableModels, requested) {
    const term = requested.trim();
    if (!term)
        return { error: 'Model must not be empty.' };
    const lowerTerm = term.toLowerCase();
    const matches = availableModels.filter((model) => {
        const lowerId = model.id.toLowerCase();
        const lowerRef = formatModelRef(model).toLowerCase();
        return lowerRef === lowerTerm || lowerId === lowerTerm;
    });
    if (matches.length === 1)
        return { model: matches[0] };
    if (matches.length === 0) {
        return {
            error: `Unknown model "${requested}". Available models: ${availableModels.map(formatModelRef).join(', ')}.`
        };
    }
    return {
        error: `Ambiguous model "${requested}". Use one of: ${matches.map(formatModelRef).join(', ')}.`
    };
}
/** Default model IDs for each known provider */
export const defaultModelPerProvider = {
    anthropic: SHORTCUT_MODEL_ID.ClaudeOpus48,
    openai: 'gpt-5.1-codex',
    'azure-openai-responses': 'gpt-5.2',
    google: 'gemini-2.5-pro',
    'google-vertex': 'gemini-3-pro-preview',
    openrouter: 'openai/gpt-5.1-codex',
    'vercel-ai-gateway': 'anthropic/claude-opus-4-8',
    xai: 'grok-4-fast-non-reasoning',
    groq: 'openai/gpt-oss-120b',
    cerebras: 'zai-glm-4.6',
    zai: 'glm-4.6',
    minimax: 'MiniMax-M2.1',
    'minimax-cn': 'MiniMax-M2.1',
    huggingface: 'moonshotai/Kimi-K2.5',
    opencode: SHORTCUT_MODEL_ID.ClaudeOpus48,
    'kimi-coding': 'kimi-k2-thinking'
};
/**
 * Helper to check if a model ID looks like an alias (no date suffix)
 * Dates are typically in format: -20241022 or -20250929
 */
function isAlias(id) {
    // Check if ID ends with -latest
    if (id.endsWith('-latest'))
        return true;
    // Check if ID ends with a date pattern (-YYYYMMDD)
    const datePattern = /-\d{8}$/;
    return !datePattern.test(id);
}
/**
 * Try to match a pattern to a model from the available models list.
 * Returns the matched model or undefined if no match found.
 */
/** Match a model pattern against the available models, supporting provider/id and fuzzy matches. */
function tryMatchModel(modelPattern, availableModels) {
    // Check for provider/modelId format (provider is everything before the first /)
    const slashIndex = modelPattern.indexOf('/');
    if (slashIndex !== -1) {
        const provider = modelPattern.substring(0, slashIndex);
        const modelId = modelPattern.substring(slashIndex + 1);
        const providerMatch = availableModels.find((m) => m.provider.toLowerCase() === provider.toLowerCase() &&
            m.id.toLowerCase() === modelId.toLowerCase());
        if (providerMatch) {
            return providerMatch;
        }
        // Provider was explicitly specified but model not found for that provider.
        // Do NOT fall through to matching other providers — respect the explicit provider.
        return undefined;
    }
    // Check for exact ID match (case-insensitive)
    const exactMatch = availableModels.find((m) => m.id.toLowerCase() === modelPattern.toLowerCase());
    if (exactMatch) {
        return exactMatch;
    }
    // No exact match - fall back to partial matching
    const matches = availableModels.filter((m) => m.id.toLowerCase().includes(modelPattern.toLowerCase()) ||
        m.name?.toLowerCase().includes(modelPattern.toLowerCase()));
    if (matches.length === 0) {
        return undefined;
    }
    // Separate into aliases and dated versions
    const aliases = matches.filter((m) => isAlias(m.id));
    const datedVersions = matches.filter((m) => !isAlias(m.id));
    if (aliases.length > 0) {
        // Prefer alias - if multiple aliases, pick the one that sorts highest
        aliases.sort((a, b) => b.id.localeCompare(a.id));
        return aliases[0];
    }
    else {
        // No alias found, pick latest dated version
        datedVersions.sort((a, b) => b.id.localeCompare(a.id));
        return datedVersions[0];
    }
}
/**
 * Parse a pattern to extract model and thinking level.
 * Handles models with colons in their IDs (e.g., OpenRouter's :exacto suffix).
 *
 * Algorithm:
 * 1. Try to match full pattern as a model
 * 2. If found, return it with "off" thinking level
 * 3. If not found and has colons, split on last colon:
 *    - If suffix is valid thinking level, use it and recurse on prefix
 *    - If suffix is invalid, warn and recurse on prefix with "off"
 *
 * @internal Exported for testing
 */
export function parseModelPattern(pattern, availableModels, options) {
    // Try exact match first
    const exactMatch = tryMatchModel(pattern, availableModels);
    if (exactMatch) {
        return { model: exactMatch, thinkingLevel: undefined, warning: undefined };
    }
    // No match - try splitting on last colon if present
    const lastColonIndex = pattern.lastIndexOf(':');
    if (lastColonIndex === -1) {
        // No colons, pattern simply doesn't match any model
        return { model: undefined, thinkingLevel: undefined, warning: undefined };
    }
    const prefix = pattern.substring(0, lastColonIndex);
    const suffix = pattern.substring(lastColonIndex + 1);
    if (isValidThinkingLevel(suffix)) {
        // Valid thinking level - recurse on prefix and use this level
        const result = parseModelPattern(prefix, availableModels, options);
        if (result.model) {
            // Only use this thinking level if no warning from inner recursion
            return {
                model: result.model,
                thinkingLevel: result.warning ? undefined : suffix,
                warning: result.warning
            };
        }
        return result;
    }
    else {
        // Invalid suffix
        const allowFallback = options?.allowInvalidThinkingLevelFallback ?? true;
        if (!allowFallback) {
            // In strict mode (CLI --model parsing), treat it as part of the model id and fail.
            // This avoids accidentally resolving to a different model.
            return { model: undefined, thinkingLevel: undefined, warning: undefined };
        }
        // Scope mode: recurse on prefix and warn
        const result = parseModelPattern(prefix, availableModels, options);
        if (result.model) {
            return {
                model: result.model,
                thinkingLevel: undefined,
                warning: `Invalid thinking level "${suffix}" in pattern "${pattern}". Using default instead.`
            };
        }
        return result;
    }
}
/**
 * Resolve a single model from CLI flags.
 *
 * Supports:
 * - --provider <provider> --model <pattern>
 * - --model <provider>/<pattern>
 * - Fuzzy matching (same rules as model scoping: exact id, then partial id/name)
 *
 * Note: This does not apply the thinking level by itself, but it may *parse* and
 * return a thinking level from "<pattern>:<thinking>" so the caller can apply it.
 */
export function resolveCliModel(options) {
    const { cliProvider, cliModel, modelRegistry } = options;
    if (!cliModel) {
        return { model: undefined, warning: undefined, error: undefined };
    }
    // Important: use *all* models here, not just models with pre-configured auth.
    // This allows "--api-key" to be used for first-time setup.
    const availableModels = modelRegistry.getAll();
    if (availableModels.length === 0) {
        return {
            model: undefined,
            warning: undefined,
            error: 'No models available. Check your installation or add models to models.json.'
        };
    }
    // Build canonical provider lookup (case-insensitive)
    const providerMap = new Map();
    for (const m of availableModels) {
        providerMap.set(m.provider.toLowerCase(), m.provider);
    }
    let provider = cliProvider ? providerMap.get(cliProvider.toLowerCase()) : undefined;
    if (cliProvider && !provider) {
        return {
            model: undefined,
            warning: undefined,
            error: `Unknown provider "${cliProvider}". Use --list-models to see available providers/models.`
        };
    }
    // If no explicit --provider, try to interpret "provider/model" format first.
    // When the prefix before the first slash matches a known provider, prefer that
    // interpretation over matching models whose IDs literally contain slashes
    // (e.g. "zai/glm-5" should resolve to provider=zai, model=glm-5, not to a
    // vercel-ai-gateway model with id "zai/glm-5").
    let pattern = cliModel;
    let inferredProvider = false;
    if (!provider) {
        const slashIndex = cliModel.indexOf('/');
        if (slashIndex !== -1) {
            const maybeProvider = cliModel.substring(0, slashIndex);
            const canonical = providerMap.get(maybeProvider.toLowerCase());
            if (canonical) {
                provider = canonical;
                pattern = cliModel.substring(slashIndex + 1);
                inferredProvider = true;
            }
        }
    }
    // If no provider was inferred from the slash, try exact matches without provider inference.
    // This handles models whose IDs naturally contain slashes (e.g. OpenRouter-style IDs).
    if (!provider) {
        const lower = cliModel.toLowerCase();
        const exact = availableModels.find((m) => m.id.toLowerCase() === lower || `${m.provider}/${m.id}`.toLowerCase() === lower);
        if (exact) {
            return { model: exact, warning: undefined, thinkingLevel: undefined, error: undefined };
        }
    }
    if (cliProvider && provider) {
        // If both were provided, tolerate --model <provider>/<pattern> by stripping the provider prefix
        const prefix = `${provider}/`;
        if (cliModel.toLowerCase().startsWith(prefix.toLowerCase())) {
            pattern = cliModel.substring(prefix.length);
        }
    }
    const candidates = provider
        ? availableModels.filter((m) => m.provider === provider)
        : availableModels;
    const { model, thinkingLevel, warning } = parseModelPattern(pattern, candidates, {
        allowInvalidThinkingLevelFallback: false
    });
    if (model) {
        return { model, thinkingLevel, warning, error: undefined };
    }
    // If we inferred a provider from the slash but found no match within that provider,
    // fall back to matching the full input as a raw model id across all models.
    // This handles OpenRouter-style IDs like "openai/gpt-4o:extended" where "openai"
    // looks like a provider but the full string is actually a model id on openrouter.
    if (inferredProvider) {
        const lower = cliModel.toLowerCase();
        const exact = availableModels.find((m) => m.id.toLowerCase() === lower || `${m.provider}/${m.id}`.toLowerCase() === lower);
        if (exact) {
            return { model: exact, warning: undefined, thinkingLevel: undefined, error: undefined };
        }
        // Also try parseModelPattern on the full input against all models
        const fallback = parseModelPattern(cliModel, availableModels, {
            allowInvalidThinkingLevelFallback: false
        });
        if (fallback.model) {
            return {
                model: fallback.model,
                thinkingLevel: fallback.thinkingLevel,
                warning: fallback.warning,
                error: undefined
            };
        }
    }
    const display = provider ? `${provider}/${pattern}` : cliModel;
    return {
        model: undefined,
        thinkingLevel: undefined,
        warning,
        error: `Model "${display}" not found. Use --list-models to see available models.`
    };
}
/**
 * Find the initial model to use based on priority:
 * 1. CLI args (provider + model)
 * 2. Saved default from settings
 * 3. First available model with valid API key
 */
export async function findInitialModel(options) {
    const { cliProvider, cliModel, defaultProvider, defaultModelId, defaultThinkingLevel, modelRegistry } = options;
    let model;
    let thinkingLevel = DEFAULT_THINKING_LEVEL;
    // 1. CLI args take priority
    if (cliProvider && cliModel) {
        const found = modelRegistry.find(cliProvider, cliModel);
        if (!found) {
            console.error(chalk.red(`Model ${cliProvider}/${cliModel} not found`));
            process.exit(1);
        }
        return { model: found, thinkingLevel: DEFAULT_THINKING_LEVEL, fallbackMessage: undefined };
    }
    // 2. Try saved default from settings
    if (defaultProvider && defaultModelId) {
        const found = modelRegistry.find(defaultProvider, defaultModelId);
        if (found) {
            model = found;
            if (defaultThinkingLevel) {
                thinkingLevel = defaultThinkingLevel;
            }
            return { model, thinkingLevel, fallbackMessage: undefined };
        }
    }
    // 4. Try first available model with valid API key
    const availableModels = await modelRegistry.getAvailable();
    if (availableModels.length > 0) {
        // Try to find a default model from known providers
        for (const provider of Object.keys(defaultModelPerProvider)) {
            const defaultId = defaultModelPerProvider[provider];
            const match = availableModels.find((m) => m.provider === provider && m.id === defaultId);
            if (match) {
                return { model: match, thinkingLevel: DEFAULT_THINKING_LEVEL, fallbackMessage: undefined };
            }
        }
        // If no default found, use first available
        return {
            model: availableModels[0],
            thinkingLevel: DEFAULT_THINKING_LEVEL,
            fallbackMessage: undefined
        };
    }
    // 5. No model found
    return { model: undefined, thinkingLevel: DEFAULT_THINKING_LEVEL, fallbackMessage: undefined };
}
//# sourceMappingURL=model-resolver.js.map