# What is this?
## Common checks for /v1/models and `/model/info`
import copy
from collections.abc import Sequence
from typing import Any, Final

import litellm
from litellm._logging import verbose_proxy_logger
from litellm.litellm_core_utils.credential_accessor import CredentialAccessor
from litellm.proxy._types import SpecialModelNames, UserAPIKeyAuth
from litellm.repositories.object_permission_repository import ObjectPermissionRepository
from litellm.router import Router
from litellm.router_utils.fallback_event_handlers import get_fallback_model_group
from litellm.types.router import CredentialLiteLLMParams, LiteLLM_Params
from litellm.types.utils import LlmProviders
from litellm.utils import get_valid_models

_CREDENTIAL_LITELLM_PARAM_FIELDS = set(CredentialLiteLLMParams.model_fields)


_CREDENTIAL_LITELLM_PARAM_FIELDS = set(CredentialLiteLLMParams.model_fields)


def _check_wildcard_routing(model: str) -> bool:
    """
    Returns True if a model is a provider wildcard.

    eg:
    - anthropic/*
    - openai/*
    - *
    """
    if "*" in model:
        return True
    return False


def get_provider_models(provider: str, litellm_params: LiteLLM_Params | None = None) -> list[str] | None:
    """
    Returns the list of known models by provider
    """
    if provider == "*":
        return get_valid_models(litellm_params=litellm_params)

    if provider in litellm.models_by_provider:
        provider_models: Final = get_valid_models(custom_llm_provider=provider, litellm_params=litellm_params)
        return provider_models
    return None


def _get_models_from_access_groups(
    model_access_groups: dict[str, list[str]],
    all_models: list[str],
    include_model_access_groups: bool | None = False,
    proxy_model_list: Sequence[str] | None = None,
) -> list[str]:
    # a grant naming both a deployed model and an access group means both at runtime
    # (_check_model_access_helper unions them), so listings must keep the literal too
    deployed_model_names: Final = frozenset(proxy_model_list or ())
    kept_models: Final = [
        model
        for model in all_models
        if model not in model_access_groups or include_model_access_groups or model in deployed_model_names
    ]
    member_models: Final = [
        member for model in all_models if model in model_access_groups for member in model_access_groups[model]
    ]
    return kept_models + member_models


async def get_mcp_server_ids(
    user_api_key_dict: UserAPIKeyAuth,
) -> list[str]:
    """
    Returns the list of MCP server ids for a given key by querying the object_permission table
    """
    from litellm.proxy.proxy_server import prisma_client

    if prisma_client is None:
        return []

    if user_api_key_dict.object_permission_id is None:
        return []

    # Make a direct SQL query to get just the mcp_servers
    try:
        result: Final = await ObjectPermissionRepository(prisma_client).table.find_unique(
            where={"object_permission_id": user_api_key_dict.object_permission_id},
        )
        if result and result.mcp_servers:
            return result.mcp_servers
        return []
    except Exception:
        return []


def get_key_models(
    user_api_key_dict: UserAPIKeyAuth,
    proxy_model_list: list[str],
    model_access_groups: dict[str, list[str]],
    include_model_access_groups: bool | None = False,
    only_model_access_groups: bool | None = False,
) -> list[str]:
    """
    Returns:
    - List of model name strings
    - Empty list if no models set
    - If model_access_groups is provided, only return models that are in the access groups
    - If include_model_access_groups is True, it includes the 'keys' of the model_access_groups
      in the response - {"beta-models": ["gpt-4", "claude-v1"]} -> returns 'beta-models'
    """
    all_models: list[str] = []
    if len(user_api_key_dict.models) > 0:
        all_models = list(user_api_key_dict.models)  # copy to avoid mutating cached objects
        if SpecialModelNames.all_team_models.value in all_models:
            all_models = list(user_api_key_dict.team_models)
            if SpecialModelNames.all_team_models.value in all_models:
                all_models = [model for model in all_models if model != SpecialModelNames.all_team_models.value]
                all_models.extend(proxy_model_list)
                if include_model_access_groups:
                    all_models.extend(model_access_groups.keys())
        if SpecialModelNames.all_proxy_models.value in all_models:
            all_models = list(proxy_model_list)  # copy to avoid mutating caller's list
            if include_model_access_groups:
                all_models.extend(model_access_groups.keys())

    all_models = _get_models_from_access_groups(
        model_access_groups=model_access_groups,
        all_models=all_models,
        include_model_access_groups=include_model_access_groups,
        proxy_model_list=proxy_model_list,
    )

    # deduplicate while preserving order
    all_models = list(dict.fromkeys(all_models))

    verbose_proxy_logger.debug("ALL KEY MODELS - %s", len(all_models))
    return all_models


def get_team_models(
    team_models: list[str],
    proxy_model_list: list[str],
    model_access_groups: dict[str, list[str]],
    include_model_access_groups: bool | None = False,
) -> list[str]:
    """
    Returns:
    - List of model name strings
    - Empty list if no models set
    - If model_access_groups is provided, only return models that are in the access groups
    """
    all_models_set: Final[set[str]] = set()
    if len(team_models) > 0:
        all_models_set.update(team_models)
        if SpecialModelNames.all_team_models.value in all_models_set:
            all_models_set.update(team_models)
            # GH#30619: expand all-team-models sentinel
            # to the actual proxy model list
            all_models_set.discard(SpecialModelNames.all_team_models.value)
            all_models_set.update(proxy_model_list)
            if include_model_access_groups:
                all_models_set.update(model_access_groups.keys())
        if SpecialModelNames.all_proxy_models.value in all_models_set:
            all_models_set.update(proxy_model_list)
            if include_model_access_groups:
                all_models_set.update(model_access_groups.keys())

    all_models = _get_models_from_access_groups(
        model_access_groups=model_access_groups,
        all_models=list(all_models_set),
        include_model_access_groups=include_model_access_groups,
        proxy_model_list=proxy_model_list,
    )

    # deduplicate while preserving order
    all_models = list(dict.fromkeys(all_models))

    verbose_proxy_logger.debug("ALL TEAM MODELS - %s", len(all_models))
    return all_models


def get_complete_model_list(
    key_models: Sequence[str],
    team_models: Sequence[str],
    proxy_model_list: list[str],
    user_model: str | None,
    infer_model_from_keys: bool | None,
    return_wildcard_routes: bool | None = False,
    llm_router: Router | None = None,
    model_access_groups: dict[str, list[str]] = {},
    include_model_access_groups: bool | None = False,
    only_model_access_groups: bool | None = False,
    team_id: str | None = None,
) -> list[str]:
    """Logic for returning complete model list for a given key + team pair"""

    """
    - If key list is empty -> defer to team list
    - If team list is empty -> defer to proxy model list

    If list contains wildcard -> return known provider models
    """

    unique_models: Final = []

    def append_unique(models):
        for model in models:
            if model not in unique_models and model != SpecialModelNames.no_default_models.value:
                unique_models.append(model)

    if key_models:
        append_unique(key_models)
    elif team_models:
        append_unique(team_models)
    else:
        append_unique(proxy_model_list)
        if include_model_access_groups:
            append_unique(list(model_access_groups.keys()))  # TODO: keys order

        if user_model:
            append_unique([user_model])

        if infer_model_from_keys:
            valid_models: Final = get_valid_models()
            append_unique(valid_models)

    if only_model_access_groups:
        model_access_groups_to_return: Final[list[str]] = []
        for model in unique_models:
            if model in model_access_groups:
                model_access_groups_to_return.append(model)
        return model_access_groups_to_return

    all_wildcard_models: Final = _get_wildcard_models(
        unique_models=unique_models,
        return_wildcard_routes=return_wildcard_routes,
        llm_router=llm_router,
        team_id=team_id,
    )

    complete_model_list: Final = unique_models + all_wildcard_models

    return complete_model_list


def _hydrate_litellm_credential_name(
    litellm_params: LiteLLM_Params | None,
) -> LiteLLM_Params | None:
    if litellm_params is None or litellm_params.litellm_credential_name is None:
        return litellm_params

    credential_values: Final = CredentialAccessor.get_credential_values(litellm_params.litellm_credential_name)
    if not credential_values:
        return litellm_params

    litellm_params = litellm_params.model_copy()
    for key, value in credential_values.items():
        if key in _CREDENTIAL_LITELLM_PARAM_FIELDS and getattr(litellm_params, key, None) is None:
            setattr(litellm_params, key, value)
    litellm_params.litellm_credential_name = None
    return litellm_params


def get_known_models_from_wildcard(wildcard_model: str, litellm_params: LiteLLM_Params | None = None) -> list[str]:
    wildcard_model_to_expand: Final = (
        litellm_params.model
        if wildcard_model == "*"
        and litellm_params is not None
        and _check_wildcard_routing(litellm_params.model)
        and "/" in litellm_params.model
        else wildcard_model
    )
    try:
        wildcard_provider_prefix, wildcard_suffix = wildcard_model_to_expand.split("/", 1)
    except ValueError:  # safely fail
        return []

    # Use provider from litellm_params when available, otherwise from wildcard prefix
    # (e.g., "openai" from "openai/*" - needed for BYOK where wildcard isn't in router)
    if litellm_params is not None:
        try:
            provider = litellm_params.model.split("/", 1)[0]
        except ValueError:
            provider = wildcard_provider_prefix
    else:
        provider = wildcard_provider_prefix

    litellm_params = _hydrate_litellm_credential_name(litellm_params)

    wildcard_models = get_provider_models(provider=provider, litellm_params=litellm_params)

    if wildcard_models is None:
        return []
    if wildcard_suffix != "*":
        ## CHECK IF PARTIAL FILTER e.g. `gemini-*`
        model_prefix: Final = wildcard_suffix.replace("*", "")

        is_partial_filter: Final = any(wc_model.startswith(model_prefix) for wc_model in wildcard_models)
        if is_partial_filter:
            filtered_wildcard_models = [wc_model for wc_model in wildcard_models if wc_model.startswith(model_prefix)]
            wildcard_models = filtered_wildcard_models
        else:
            # add model prefix to wildcard models
            wildcard_models = [f"{model_prefix}{model}" for model in wildcard_models]

    known_providers: Final = {provider.value for provider in LlmProviders}
    suffix_appended_wildcard_models: Final = []
    for model in wildcard_models:
        if not model.startswith(wildcard_provider_prefix):
            # `get_provider_models` returns provider-prefixed ids (e.g. "ollama/gemma3:1b").
            # When the wildcard uses a custom prefix (e.g. "ollama_server1/*" to distinguish
            # multiple instances), replace that existing provider prefix instead of stacking
            # both, which would otherwise yield an uncallable "ollama_server1/ollama/gemma3:1b".
            # Only strip the leading segment when it is a known provider, so ids whose first
            # segment is an org rather than a provider (e.g. "meta-llama/Llama-3-8B") keep it.
            leading, sep, model_suffix = model.partition("/")
            if sep and leading in known_providers:
                model = f"{wildcard_provider_prefix}/{model_suffix}"
            else:
                model = f"{wildcard_provider_prefix}/{model}"
        suffix_appended_wildcard_models.append(model)
    return suffix_appended_wildcard_models or []


def expand_wildcard_deployments_for_model_info(
    deployments: list[dict[str, Any]],
) -> list[dict[str, Any]]:
    """Expand wildcard deployments into one row per known provider model.

    PR #30025 changed /model/info to read from llm_router.model_list (correct,
    so team-scoped rows are included). This function restores wildcard expansion
    on top of that: a wildcard deployment like model_name="*" / litellm_params.model="openai/*"
    becomes one entry per known openai model, matching /v1/models behaviour.
    """
    expanded: Final[list[dict[str, Any]]] = []
    for deployment in deployments:
        model_name = str(deployment.get("model_name") or "")
        raw_params = deployment.get("litellm_params")
        litellm_params_dict: dict[str, Any] = raw_params if isinstance(raw_params, dict) else {}
        litellm_model = str(litellm_params_dict.get("model") or "")

        # Determine the wildcard pattern to expand.
        # Branch order matters: only fall to litellm_model when model_name is
        # also a wildcard, so a concrete model_name is never overwritten.
        if _check_wildcard_routing(model_name) and "/" in model_name:
            wildcard_pattern = model_name
        elif _check_wildcard_routing(model_name) and _check_wildcard_routing(litellm_model):
            wildcard_pattern = litellm_model
        elif _check_wildcard_routing(model_name):
            wildcard_pattern = model_name
        else:
            expanded.append(deployment)
            continue

        try:
            litellm_params = LiteLLM_Params.model_validate(litellm_params_dict) if litellm_params_dict else None
        except Exception:
            expanded.append(deployment)
            continue
        expanded_names = get_known_models_from_wildcard(
            wildcard_model=wildcard_pattern,
            litellm_params=litellm_params,
        )
        if not expanded_names:
            expanded.append(deployment)
            continue

        for name in expanded_names:
            row = copy.deepcopy(deployment)
            row["model_name"] = name
            params = row.get("litellm_params")
            if isinstance(params, dict):
                params["model"] = name
            expanded.append(row)

    return expanded


def _get_wildcard_models(
    unique_models: list[str],
    return_wildcard_routes: bool | None = False,
    llm_router: Router | None = None,
    team_id: str | None = None,
) -> list[str]:
    models_to_remove: Final = set()
    all_wildcard_models: Final = []
    for model in unique_models:
        if _check_wildcard_routing(model=model):
            if return_wildcard_routes:  # will add the wildcard route to the list eg: anthropic/*.
                all_wildcard_models.append(model)

            ## get litellm params from model
            if llm_router is not None:
                model_list = llm_router.get_model_list(model_name=model, team_id=team_id)
                if model_list:
                    for router_model in model_list:
                        wildcard_models = get_known_models_from_wildcard(
                            wildcard_model=model,
                            litellm_params=LiteLLM_Params(**router_model["litellm_params"]),
                        )
                        all_wildcard_models.extend(wildcard_models)
                else:
                    # Router has no deployment for this wildcard (e.g., BYOK team models)
                    # Fall back to expanding from known provider models
                    wildcard_models = get_known_models_from_wildcard(wildcard_model=model, litellm_params=None)
                    if wildcard_models:
                        models_to_remove.add(model)
                        all_wildcard_models.extend(wildcard_models)
            else:
                # get all known provider models
                wildcard_models = get_known_models_from_wildcard(wildcard_model=model, litellm_params=None)

                if wildcard_models:
                    models_to_remove.add(model)
                    all_wildcard_models.extend(wildcard_models)

    for model in models_to_remove:
        unique_models.remove(model)

    return all_wildcard_models


def get_all_fallbacks(
    model: str,
    llm_router: Router | None = None,
    fallback_type: str = "general",
) -> list[str]:
    """
    Get all fallbacks for a given model from the router's fallback configuration.

    Args:
        model: The model name to get fallbacks for
        llm_router: The LiteLLM router instance
        fallback_type: Type of fallback ("general", "context_window", "content_policy")

    Returns:
        List of fallback model names. Empty list if no fallbacks found.
    """
    if llm_router is None:
        return []

    # Get the appropriate fallback list based on type
    fallbacks_config: list = []
    if fallback_type == "general":
        fallbacks_config = getattr(llm_router, "fallbacks", [])
    elif fallback_type == "context_window":
        fallbacks_config = getattr(llm_router, "context_window_fallbacks", [])
    elif fallback_type == "content_policy":
        fallbacks_config = getattr(llm_router, "content_policy_fallbacks", [])
    else:
        verbose_proxy_logger.warning("Unknown fallback_type: %s", fallback_type)
        return []

    if not fallbacks_config:
        return []

    try:
        # Use existing function to get fallback model group
        fallback_model_group, _ = get_fallback_model_group(fallbacks=fallbacks_config, model_group=model)

        if fallback_model_group is None:
            return []

        return fallback_model_group
    except Exception as e:
        verbose_proxy_logger.error("Error getting fallbacks for model %s: %s", model, e)
        return []
