#
#  Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.
#
import logging
import os
import enum
import json
from common import settings
from common.constants import (
    ActiveStatusEnum,
    LLMType,
    ModelTypeBinary,
    MINERU_DEFAULT_CONFIG,
    MINERU_ENV_KEYS,
    MISTRAL_OCR_DEFAULT_CONFIG,
    MISTRAL_OCR_ENV_KEYS,
    OPENDATALOADER_DEFAULT_CONFIG,
    OPENDATALOADER_ENV_KEYS,
    PADDLEOCR_DEFAULT_CONFIG,
    PADDLEOCR_ENV_KEYS,
    SOMARK_DEFAULT_CONFIG,
    SOMARK_ENV_KEYS,
)
from api.db.services.tenant_llm_service import TenantService
from api.db.services.tenant_model_provider_service import TenantModelProviderService
from api.db.services.tenant_model_instance_service import TenantModelInstanceService
from api.db.services.tenant_model_service import TenantModelService
from api.utils.model_utils import calculate_model_type, get_model_type_human

logger = logging.getLogger(__name__)


def _factory_model_types(llm: dict) -> list[str]:
    model_type = llm.get("model_type")
    if isinstance(model_type, list):
        return model_type
    return [model_type] if model_type else []


def _lookup_factory_llm_info(provider_name: str, pure_model_name: str, extra_fields: dict) -> dict | None:
    region = extra_fields.get("region", "default")
    if region == "intl" and provider_name.lower() == "siliconflow":
        target_factory_name = "siliconflow_intl"
    else:
        target_factory_name = provider_name
    fac_list = [f for f in settings.FACTORY_LLM_INFOS if f["name"] == target_factory_name]
    if not fac_list:
        return None
    llm_list = [llm for llm in fac_list[0]["llm"] if llm["llm_name"] == pure_model_name]
    return llm_list[0] if llm_list else None


def _decode_api_key_config(raw_api_key: str) -> tuple[str, bool | None, str | None]:
    if not raw_api_key:
        return raw_api_key, None, None

    try:
        parsed = json.loads(raw_api_key)
    except Exception:
        return raw_api_key, None, None

    if not isinstance(parsed, dict):
        return raw_api_key, None, None

    is_tools = bool(parsed["is_tools"]) if "is_tools" in parsed else None
    if set(parsed.keys()) <= {"api_key", "is_tools"}:
        return parsed.get("api_key", ""), is_tools, None

    return parsed.get("api_key", raw_api_key), is_tools, raw_api_key


def get_first_provider_model_name(tenant_id: str, provider_name: str, model_type: str | enum.Enum) -> str | None:
    model_type_bin = calculate_model_type(model_type)
    provider_obj = TenantModelProviderService.get_by_tenant_id_and_provider_name(tenant_id, provider_name)
    if not provider_obj:
        return None

    for instance_obj in TenantModelInstanceService.get_all_by_provider_id(provider_obj.id):
        if instance_obj.status != ActiveStatusEnum.ACTIVE.value:
            continue
        for model_obj in TenantModelService.get_models_by_instance_id(instance_obj.id):
            if model_obj.model_type & model_type_bin and model_obj.status == ActiveStatusEnum.ACTIVE.value:
                return f"{model_obj.model_name}@{instance_obj.instance_name}@{provider_name}"
    return None


def _collect_env_config(env_keys: list[str], default_config: dict) -> dict | None:
    config = dict(default_config)
    found = False
    for key in env_keys:
        value = os.environ.get(key)
        if value:
            found = True
            config[key] = value
    return config if found else None


def _ensure_ocr_provider_from_env(tenant_id: str, provider_name: str, model_name: str, config: dict | None) -> str | None:
    if not config:
        return None

    provider_obj = TenantModelProviderService.get_by_tenant_id_and_provider_name(tenant_id, provider_name)
    if not provider_obj:
        TenantModelProviderService.insert(tenant_id=tenant_id, provider_name=provider_name)
        provider_obj = TenantModelProviderService.get_by_tenant_id_and_provider_name(tenant_id, provider_name)

    api_key = json.dumps(config)
    instance_obj = TenantModelInstanceService.get_by_provider_id_and_api_key(provider_obj.id, api_key)
    if not instance_obj:
        instance_obj = TenantModelInstanceService.create_instance(
            provider_id=provider_obj.id,
            instance_name=model_name,
            api_key=api_key,
            extra="{}",
        )

    model_obj = TenantModelService.get_by_provider_id_and_instance_id_and_model_type_and_model_name(
        provider_obj.id,
        instance_obj.id,
        LLMType.OCR.value,
        model_name,
    )
    if not model_obj:
        TenantModelService.insert(
            model_name=model_name,
            provider_id=provider_obj.id,
            instance_id=instance_obj.id,
            model_type=ModelTypeBinary.OCR.value,
            extra=json.dumps({"max_tokens": 0}),
        )

    return f"{model_name}@{instance_obj.instance_name}@{provider_name}"


def ensure_mineru_from_env(tenant_id: str) -> str | None:
    return _ensure_ocr_provider_from_env(
        tenant_id,
        "MinerU",
        "mineru-from-env",
        _collect_env_config(MINERU_ENV_KEYS, MINERU_DEFAULT_CONFIG),
    )


def ensure_paddleocr_from_env(tenant_id: str) -> str | None:
    return _ensure_ocr_provider_from_env(
        tenant_id,
        "PaddleOCR",
        "paddleocr-from-env",
        _collect_env_config(PADDLEOCR_ENV_KEYS, PADDLEOCR_DEFAULT_CONFIG),
    )


def get_tenant_default_model_by_type(tenant_id: str, model_type: str | enum.Enum):
    exist, tenant = TenantService.get_by_id(tenant_id)
    if not exist:
        raise LookupError("Tenant not found")
    model_type_val = model_type if isinstance(model_type, str) else model_type.value
    model_id: str | None = None
    model_name: str = ""
    match model_type_val:
        case LLMType.EMBEDDING.value:
            model_id = tenant.tenant_embd_id
            model_name = tenant.embd_id
        case LLMType.ASR.value:
            model_id = tenant.tenant_asr_id
            model_name = tenant.asr_id
        case LLMType.VISION.value:
            model_id = tenant.tenant_img2txt_id
            model_name = tenant.img2txt_id
        case LLMType.CHAT.value:
            model_id = tenant.tenant_llm_id
            model_name = tenant.llm_id
        case LLMType.RERANK.value:
            model_id = tenant.tenant_rerank_id
            model_name = tenant.rerank_id
        case LLMType.TTS.value:
            model_id = tenant.tenant_tts_id
            model_name = tenant.tts_id
        case LLMType.OCR.value:
            raise Exception("OCR model name is required")
        case _:
            raise Exception(f"Unknown model type {model_type}")
    if not model_name:
        raise Exception(f"No default {model_type} model is set.")
    # Prefer resolving by tenant_model.id when available
    if model_id:
        try:
            return get_model_config_by_id(tenant_id, model_type, model_id)
        except LookupError:
            logger.warning("tenant_model id=%s not found, falling back to model_name lookup for %s", model_id, model_name)
    return resolve_model_config(tenant_id, model_type, model_name)


def split_model_name(model_name: str):
    # Parse model_name: {model_name} or {model_name}@{factory_name} or {model_name}@{instance_name}@{factory_name}
    #
    # The composite key is right-anchored on the provider: the *last* '@'-separated
    # field is the factory/provider, the second-to-last is the instance (when
    # present), and everything to the left is the bare model name. Some model
    # names legitimately contain '@' characters themselves (e.g. LM Studio
    # embedding model IDs such as `text-embedding-nomic-embed-text-v1.5@q8_0`),
    # which produces composite keys like
    # `text-embedding-nomic-embed-text-v1.5@q8_0@lmstudio@LM-Studio`.
    # Use rsplit with maxsplit=2 from the right so any '@' characters embedded
    # in the leftmost model-name component are preserved as part of that name.
    parts = model_name.rsplit("@", 2)
    n = len(parts)
    if n == 3:
        pure_model_name, instance_name, provider_name = parts
    elif n == 2:
        pure_model_name, provider_name = parts
        instance_name = "default"
    else:
        pure_model_name = parts[0]
        provider_name = ""
        instance_name = ""
    return pure_model_name, instance_name, provider_name


def _resolve_instance_for_model(provider_obj, instance_name: str, model_name: str):
    instance_obj = TenantModelInstanceService.get_by_provider_id_and_instance_name(provider_obj.id, instance_name)
    if instance_obj:
        return instance_obj
    if instance_name != "default":
        raise LookupError(f"Instance {instance_name} not found for model {model_name}.")

    active_instances = [inst for inst in TenantModelInstanceService.get_all_by_provider_id(provider_obj.id) if inst.status == ActiveStatusEnum.ACTIVE.value]
    if len(active_instances) == 1:
        logger.warning(
            "Model instance fallback applied for legacy default instance name",
            extra={
                "provider_name": provider_obj.provider_name,
                "requested_instance_name": instance_name,
                "resolved_instance_name": active_instances[0].instance_name,
                "model_name": model_name,
            },
        )
        return active_instances[0]

    raise LookupError(f"Instance {instance_name} not found for model {model_name}.")


def resolve_model_config(tenant_id, model_type: str | enum.Enum, model_ref: str):
    try:
        return get_model_config_by_id(tenant_id, model_type, model_ref)
    except LookupError:
        return get_model_config_from_provider_instance(tenant_id, model_type, model_ref)


def get_model_config_from_provider_instance(tenant_id, model_type: str | enum.Enum, model_name: str):
    pure_model_name, instance_name, provider_name = split_model_name(model_name)
    model_type_val = model_type if isinstance(model_type, str) else model_type.value
    # Builtin embedding model
    compose_profiles = os.getenv("COMPOSE_PROFILES", "")
    is_tei_builtin_embedding = (
        model_type_val == LLMType.EMBEDDING.value and "tei-" in compose_profiles and pure_model_name == os.getenv("TEI_MODEL", "") and (provider_name == "Builtin" or not provider_name)
    )
    if is_tei_builtin_embedding:
        # configured local embedding model
        embedding_cfg = settings.EMBEDDING_CFG
        return {
            "llm_factory": "Builtin",
            "api_key": embedding_cfg["api_key"],
            "llm_name": pure_model_name,
            "api_base": embedding_cfg["base_url"],
            "model_type": LLMType.EMBEDDING.value,
        }

    provider_obj = TenantModelProviderService.get_by_tenant_id_and_provider_name(tenant_id, provider_name)
    if not provider_obj:
        raise LookupError(f"Provider {provider_name} not found for model {model_name}.")
    instance_obj = _resolve_instance_for_model(provider_obj, instance_name, model_name)
    model_obj = TenantModelService.get_by_provider_id_and_instance_id_and_model_type_and_model_name(provider_obj.id, instance_obj.id, model_type_val, pure_model_name)

    api_key, is_tool, api_key_payload = _decode_api_key_config(instance_obj.api_key)
    extra_fields = json.loads(instance_obj.extra) if instance_obj.extra else {}

    if model_obj:
        if model_obj.status == ActiveStatusEnum.INACTIVE.value:
            raise LookupError(f"Model {model_name} is disabled.")
        if model_obj.status == ActiveStatusEnum.UNSUPPORTED.value:
            raise LookupError(f"Model {model_name} cannot be used as {model_type_val} model.")

        model_extra = json.loads(model_obj.extra) if model_obj.extra else {}
        llm_info = _lookup_factory_llm_info(provider_obj.provider_name, pure_model_name, extra_fields)
        if "max_tokens" in model_extra:
            max_tokens = model_extra["max_tokens"]
        else:
            max_tokens = (llm_info or {}).get("max_tokens", 8192)
        model_config = {
            "llm_factory": provider_obj.provider_name,
            "api_key": api_key,
            "llm_name": model_obj.model_name,
            "api_base": extra_fields.get("base_url", ""),
            "model_type": model_type_val,
            "is_tools": model_extra.get("is_tools", is_tool),
            "max_tokens": max_tokens,
        }
        if provider_name.lower() == "somark":
            # SoMark/OCR factories read parser config (somark_*, parse_method, ...)
            # from model_config["extra"]; see tenant_llm_service.LLMBundle OCR path.
            model_config["extra"] = model_extra

        if api_key_payload is not None:
            model_config["api_key_payload"] = api_key_payload

        return model_config
    else:
        raise LookupError(f"Model {model_name} not found for model {model_type_val}")


def get_model_config_by_id(tenant_id: str, model_type: str | enum.Enum, model_id: str):
    """Get model config from tenant_model by its id (CharField PK)."""
    model_type_val = model_type if isinstance(model_type, str) else model_type.value
    model_type_bin = calculate_model_type(model_type_val)
    exist, model_obj = TenantModelService.get_by_id(model_id)
    if not exist:
        raise LookupError(f"TenantModel id={model_id} not found.")
    if model_obj.status == ActiveStatusEnum.INACTIVE.value:
        raise LookupError(f"TenantModel id={model_id} is disabled.")
    if model_obj.status == ActiveStatusEnum.UNSUPPORTED.value:
        raise LookupError(f"TenantModel id={model_id} cannot be used as {model_type_val} model.")
    if not (model_obj.model_type & model_type_bin):
        raise LookupError(f"TenantModel id={model_id} cannot be used as {model_type_val} model.")

    ok, provider_obj = TenantModelProviderService.get_by_id(model_obj.provider_id)
    if not ok:
        raise LookupError(f"Provider id={model_obj.provider_id} not found for model id={model_id}.")

    # Validate that tenant_id owns the provider or is a joined tenant of the provider's owner.
    if tenant_id != provider_obj.tenant_id:
        joined_tenants = TenantService.get_joined_tenants_by_user_id(tenant_id)
        joined_tenant_ids = [t["tenant_id"] for t in joined_tenants]
        if provider_obj.tenant_id not in joined_tenant_ids:
            raise LookupError(f"Tenant {tenant_id} has no access to provider owned by tenant {provider_obj.tenant_id}.")

    ok, instance_obj = TenantModelInstanceService.get_by_id(model_obj.instance_id)
    if not ok:
        raise LookupError(f"Instance id={model_obj.instance_id} not found for model id={model_id}.")

    api_key, is_tool, api_key_payload = _decode_api_key_config(instance_obj.api_key)
    extra_fields = json.loads(instance_obj.extra) if instance_obj.extra else {}
    model_extra = json.loads(model_obj.extra) if model_obj.extra else {}

    model_config = {
        "llm_factory": provider_obj.provider_name,
        "api_key": api_key,
        "llm_name": model_obj.model_name,
        "api_base": extra_fields.get("base_url", ""),
        "model_type": model_type_val,
        "is_tools": model_extra.get("is_tools", is_tool),
        "max_tokens": model_extra.get("max_tokens") or 8192,
    }
    if provider_obj.provider_name.lower() == "somark":
        model_config["extra"] = model_extra

    if api_key_payload is not None:
        model_config["api_key_payload"] = api_key_payload

    return model_config


def resolve_model_id(tenant_id: str, model_type: str | enum.Enum, model_name: str) -> str | None:
    """Given a tenant_id, model_type and model_name (e.g. 'model@instance@provider'),
    look up the corresponding tenant_model.id. Returns None if not found."""
    pure_model_name, instance_name, provider_name = split_model_name(model_name)
    model_type_val = model_type if isinstance(model_type, str) else model_type.value

    # Builtin TEI embedding — no tenant_model row exists
    compose_profiles = os.getenv("COMPOSE_PROFILES", "")
    is_tei_builtin_embedding = (
        model_type_val == LLMType.EMBEDDING.value and "tei-" in compose_profiles and pure_model_name == os.getenv("TEI_MODEL", "") and (provider_name == "Builtin" or not provider_name)
    )
    if is_tei_builtin_embedding:
        return None

    if not provider_name:
        raise LookupError(f"Provider name is required to resolve model id for {model_name}.")

    provider_obj = TenantModelProviderService.get_by_tenant_id_and_provider_name(tenant_id, provider_name)
    if not provider_obj:
        raise LookupError(f"Provider {provider_name} not found for model {model_name}.")

    instance_obj = _resolve_instance_for_model(provider_obj, instance_name, model_name)
    model_obj = TenantModelService.get_by_provider_id_and_instance_id_and_model_type_and_model_name(provider_obj.id, instance_obj.id, model_type_val, pure_model_name)
    if not model_obj:
        raise LookupError(f"Model {model_name} not found for type {model_type_val}.")
    return model_obj.id


# Mapping from model-name field → (LLMType, tenant_model id field)
_MODEL_NAME_TO_ID_FIELD_MAP: dict[str, tuple[str, str]] = {
    "llm_id": (LLMType.CHAT, "tenant_llm_id"),
    "embd_id": (LLMType.EMBEDDING, "tenant_embd_id"),
    "rerank_id": (LLMType.RERANK, "tenant_rerank_id"),
    "asr_id": (LLMType.ASR, "tenant_asr_id"),
    "img2txt_id": (LLMType.VISION, "tenant_img2txt_id"),
    "tts_id": (LLMType.TTS, "tenant_tts_id"),
}


def ensure_tenant_model_ids_for_params(tenant_id: str, params: dict) -> dict:
    """For each model-name field present in *params*, resolve the corresponding
    tenant_model id if the id field is not already present.

    Modifies *params* in-place (adds ``tenant_*_id`` keys) and returns it.
    Silently skips resolution when the model is not found in tenant_model
    (e.g. builtin TEI embedding).

    Typical usage at API entry points:

        req = await get_request_json()
        ensure_tenant_model_ids_for_params(current_user.id, req)
        # req now has tenant_llm_id / tenant_embd_id etc. filled in
    """
    for name_field, (model_type, id_field) in _MODEL_NAME_TO_ID_FIELD_MAP.items():
        if name_field in params and id_field not in params:
            try:
                params[id_field] = resolve_model_id(tenant_id, model_type, params[name_field])
            except LookupError:
                logger.debug("Could not resolve %s → %s for tenant %s, skipping", name_field, id_field, tenant_id)
    return params


def get_api_key(tenant_id: str, model_name: str):
    # Try direct model ID (UUID) lookup first
    exist, model_obj = TenantModelService.get_by_id(model_name)
    if exist:
        # Verify tenant ownership through the provider chain
        ok, provider_obj = TenantModelProviderService.get_by_id(model_obj.provider_id)
        if not ok:
            raise LookupError(f"Provider id={model_obj.provider_id} not found for model {model_name}.")
        if tenant_id != provider_obj.tenant_id:
            joined_tenants = TenantService.get_joined_tenants_by_user_id(tenant_id)
            joined_tenant_ids = [t["tenant_id"] for t in joined_tenants]
            if provider_obj.tenant_id not in joined_tenant_ids:
                raise LookupError(f"Tenant {tenant_id} has no access to provider owned by tenant {provider_obj.tenant_id}.")

        exist_inst, instance_obj = TenantModelInstanceService.get_by_id(model_obj.instance_id)
        if not exist_inst:
            logger.warning(
                "Direct-ID resolution: instance not found | tenant_id=%s model_id=%s instance_id=%s",
                tenant_id,
                model_name,
                model_obj.instance_id,
            )
            raise LookupError(f"Instance {model_obj.instance_id} not found for model {model_name}.")
        logger.debug(
            "Direct-ID resolution: resolved | tenant_id=%s model_id=%s instance_id=%s",
            tenant_id,
            model_name,
            model_obj.instance_id,
        )
        return instance_obj.api_key

    # Fall back to name-based resolution: model[@instance]@provider
    _, instance_name, provider_name = split_model_name(model_name)

    if not provider_name:
        raise LookupError("Provider name is required.")
    provider_obj = TenantModelProviderService.get_by_tenant_id_and_provider_name(tenant_id, provider_name)
    if not provider_obj:
        raise LookupError(f"Provider {provider_name} not found.")
    instance_obj = _resolve_instance_for_model(provider_obj, instance_name, model_name)
    return instance_obj.api_key


def get_model_type_by_id(model_id: str):
    exist, model_obj = TenantModelService.get_by_id(model_id)
    if not exist:
        raise LookupError(f"TenantModel id={model_id} not found.")
    return get_model_type_human(model_obj.model_type)


def resolve_model_type(tenant_id: str, model_ref: str):
    try:
        return get_model_type_by_id(model_ref)
    except LookupError:
        return get_model_type_by_name(tenant_id, model_ref)


def get_model_type_by_name(tenant_id: str, model_name: str):
    pure_model_name, instance_name, provider_name = split_model_name(model_name)
    provider_obj = TenantModelProviderService.get_by_tenant_id_and_provider_name(tenant_id, provider_name)
    if not provider_obj:
        raise LookupError(f"Provider {provider_name} not found for model {model_name}.")
    instance_obj = TenantModelInstanceService.get_by_provider_id_and_instance_name(provider_obj.id, instance_name)
    if not instance_obj:
        raise LookupError(f"Instance {instance_name} not found for model {model_name}.")
    model_obj = TenantModelService.get_by_provider_id_and_instance_id_and_model_name(provider_obj.id, instance_obj.id, pure_model_name)
    if not model_obj:
        raise LookupError(f"Model {model_name} not found.")
    return get_model_type_human(model_obj.model_type)


def delete_models_by_instance_ids(instance_ids: list[str]):
    return TenantModelService.delete_by_instance_ids(instance_ids)


def delete_instances_by_provider_ids(provider_ids: list[str]):
    return TenantModelInstanceService.delete_by_provider_ids(provider_ids)


def ensure_opendataloader_from_env(tenant_id: str) -> str | None:
    return _ensure_ocr_provider_from_env(
        tenant_id,
        "OpenDataLoader",
        "opendataloader-from-env",
        _collect_env_config(OPENDATALOADER_ENV_KEYS, OPENDATALOADER_DEFAULT_CONFIG),
    )


def ensure_somark_from_env(tenant_id: str) -> str | None:
    return _ensure_ocr_provider_from_env(
        tenant_id,
        "SoMark",
        "somark-from-env",
        _collect_env_config(SOMARK_ENV_KEYS, SOMARK_DEFAULT_CONFIG),
    )


def get_composite_model_name_by_id(model_id: str) -> str:
    """Convert a tenant_model.id to the composite model name string
    ``model_name@instance_name@provider_name``.
    Raises LookupError if the model, instance, or provider is not found.
    """
    exist, model_obj = TenantModelService.get_by_id(model_id)
    if not exist:
        raise LookupError(f"TenantModel id={model_id} not found.")

    ok, instance_obj = TenantModelInstanceService.get_by_id(model_obj.instance_id)
    if not ok:
        raise LookupError(f"Instance id={model_obj.instance_id} not found for model id={model_id}.")

    ok, provider_obj = TenantModelProviderService.get_by_id(model_obj.provider_id)
    if not ok:
        raise LookupError(f"Provider id={model_obj.provider_id} not found for model id={model_id}.")

    return f"{model_obj.model_name}@{instance_obj.instance_name}@{provider_obj.provider_name}"


def ensure_mistral_ocr_from_env(tenant_id: str) -> str | None:
    return _ensure_ocr_provider_from_env(
        tenant_id,
        "Mistral OCR",
        "mistral-ocr-latest",
        _collect_env_config(MISTRAL_OCR_ENV_KEYS, MISTRAL_OCR_DEFAULT_CONFIG),
    )
