"""
providers/models.py — Dynamic model list for all active providers.

Anthropic has no public models API, so current models are hardcoded.
OpenRouter models are fetched from their API and cached for one hour.

Usage:
    models, default = await get_available_models()
    info           = await get_models_info()
"""
from __future__ import annotations

import logging
import time

logger = logging.getLogger(__name__)

# Current Anthropic models (update when new ones ship)
_ANTHROPIC_MODELS = [
    "anthropic:claude-fable-5",
    "anthropic:claude-opus-4-8",
    "anthropic:claude-sonnet-5",
    "anthropic:claude-haiku-4-5-20251001",
]

_ANTHROPIC_MODEL_INFO = [
    {
        "id": "anthropic:claude-fable-5",
        "provider": "anthropic",
        "bare_id": "claude-fable-5",
        "name": "Claude Fable 5",
        "context_length": 1000000,
        "description": "Anthropic's most capable widely-released model. Best for the most demanding reasoning and long-horizon agentic work.",
        "capabilities": {"vision": True, "tools": True, "online": False, "image_gen": False},
        "pricing": {"prompt_per_1m": 10.00, "completion_per_1m": 50.00},
        "architecture": {"tokenizer": "claude", "modality": "text+image->text"},
    },
    {
        "id": "anthropic:claude-opus-4-8",
        "provider": "anthropic",
        "bare_id": "claude-opus-4-8",
        "name": "Claude Opus 4.8",
        "context_length": 1000000,
        "description": "Anthropic's most capable Opus-tier model. Best for complex reasoning, nuanced writing, and sophisticated analysis.",
        "capabilities": {"vision": True, "tools": True, "online": False, "image_gen": False},
        "pricing": {"prompt_per_1m": 5.00, "completion_per_1m": 25.00},
        "architecture": {"tokenizer": "claude", "modality": "text+image->text"},
    },
    {
        "id": "anthropic:claude-sonnet-5",
        "provider": "anthropic",
        "bare_id": "claude-sonnet-5",
        "name": "Claude Sonnet 5",
        "context_length": 1000000,
        "description": "Best balance of speed and intelligence. Near-Opus quality on coding and agentic work at Sonnet cost.",
        "capabilities": {"vision": True, "tools": True, "online": False, "image_gen": False},
        "pricing": {"prompt_per_1m": 3.00, "completion_per_1m": 15.00},
        "architecture": {"tokenizer": "claude", "modality": "text+image->text"},
    },
    {
        "id": "anthropic:claude-haiku-4-5-20251001",
        "provider": "anthropic",
        "bare_id": "claude-haiku-4-5-20251001",
        "name": "Claude Haiku 4.5",
        "context_length": 200000,
        "description": "Fastest and most compact Claude model. Great for quick tasks, simple Q&A, and high-throughput workloads.",
        "capabilities": {"vision": True, "tools": True, "online": False, "image_gen": False},
        "pricing": {"prompt_per_1m": 1.00, "completion_per_1m": 5.00},
        "architecture": {"tokenizer": "claude", "modality": "text+image->text"},
    },
]

# Current OpenAI models (hardcoded — update when new ones ship)
_OPENAI_MODELS = [
    "openai:gpt-4o",
    "openai:gpt-4o-mini",
    "openai:gpt-4-turbo",
    "openai:o3-mini",
    "openai:gpt-5-image",
]

_OPENAI_MODEL_INFO = [
    {
        "id": "openai:gpt-4o",
        "provider": "openai",
        "bare_id": "gpt-4o",
        "name": "GPT-4o",
        "context_length": 128000,
        "description": "OpenAI's flagship model. Multimodal, fast, and highly capable for complex reasoning and generation tasks.",
        "capabilities": {"vision": True, "tools": True, "online": False, "image_gen": False},
        "pricing": {"prompt_per_1m": 2.50, "completion_per_1m": 10.00},
        "architecture": {"tokenizer": "cl100k", "modality": "text+image->text"},
    },
    {
        "id": "openai:gpt-4o-mini",
        "provider": "openai",
        "bare_id": "gpt-4o-mini",
        "name": "GPT-4o mini",
        "context_length": 128000,
        "description": "Fast and affordable GPT-4o variant. Great for high-throughput tasks that don't require maximum intelligence.",
        "capabilities": {"vision": True, "tools": True, "online": False, "image_gen": False},
        "pricing": {"prompt_per_1m": 0.15, "completion_per_1m": 0.60},
        "architecture": {"tokenizer": "cl100k", "modality": "text+image->text"},
    },
    {
        "id": "openai:gpt-4-turbo",
        "provider": "openai",
        "bare_id": "gpt-4-turbo",
        "name": "GPT-4 Turbo",
        "context_length": 128000,
        "description": "Previous-generation GPT-4 with 128K context window. Vision and tool use supported.",
        "capabilities": {"vision": True, "tools": True, "online": False, "image_gen": False},
        "pricing": {"prompt_per_1m": 10.00, "completion_per_1m": 30.00},
        "architecture": {"tokenizer": "cl100k", "modality": "text+image->text"},
    },
    {
        "id": "openai:o3-mini",
        "provider": "openai",
        "bare_id": "o3-mini",
        "name": "o3-mini",
        "context_length": 200000,
        "description": "OpenAI's efficient reasoning model. Excels at STEM tasks with strong tool-use support.",
        "capabilities": {"vision": False, "tools": True, "online": False, "image_gen": False},
        "pricing": {"prompt_per_1m": 1.10, "completion_per_1m": 4.40},
        "architecture": {"tokenizer": "cl100k", "modality": "text->text"},
    },
    {
        "id": "openai:gpt-5-image",
        "provider": "openai",
        "bare_id": "gpt-5-image",
        "name": "GPT-5 Image",
        "context_length": 128000,
        "description": "GPT-5 with native image generation. Produces high-quality images from text prompts with rich contextual understanding.",
        "capabilities": {"vision": True, "tools": False, "online": False, "image_gen": True},
        "pricing": {"prompt_per_1m": None, "completion_per_1m": None},
        "architecture": {"tokenizer": "cl100k", "modality": "text+image->image+text"},
    },
]

_or_raw: list[dict] = []      # full raw objects from OpenRouter /api/v1/models
_or_cache_ts: float = 0.0
_OR_CACHE_TTL = 3600  # seconds


async def _fetch_openrouter_raw(api_key: str) -> list[dict]:
    """Fetch full OpenRouter model objects, with a 1-hour in-memory cache."""
    global _or_raw, _or_cache_ts
    now = time.monotonic()
    if _or_raw and (now - _or_cache_ts) < _OR_CACHE_TTL:
        return _or_raw
    try:
        import httpx
        async with httpx.AsyncClient() as client:
            r = await client.get(
                "https://openrouter.ai/api/v1/models",
                headers={"Authorization": f"Bearer {api_key}"},
                timeout=10,
            )
            r.raise_for_status()
            data = r.json()
            _or_raw = [m for m in data.get("data", []) if m.get("id")]
            _or_cache_ts = now
            logger.info(f"[models] Fetched {len(_or_raw)} OpenRouter models")
            return _or_raw
    except Exception as e:
        logger.warning(f"[models] Failed to fetch OpenRouter models: {e}")
        return _or_raw  # return stale cache on error


async def _get_keys(user_id: str | None = None, is_admin: bool = True) -> tuple[str, str, str]:
    """Resolve anthropic + openrouter + openai keys for a user (user setting → global store)."""
    from ..database import credential_store, user_settings_store

    if user_id and not is_admin:
        # Admin may grant a user full access to system keys
        use_admin_keys = await user_settings_store.get(user_id, "use_admin_keys")
        if not use_admin_keys:
            ant_key = await user_settings_store.get(user_id, "anthropic_api_key") or ""
            oai_key = await user_settings_store.get(user_id, "openai_api_key") or ""
            # Non-admin with no own OR key: fall back to global (free models only)
            own_or = await user_settings_store.get(user_id, "openrouter_api_key")
            or_key = own_or or await credential_store.get("system:openrouter_api_key") or ""
            return ant_key, or_key, oai_key

    # Admin, anonymous, or user granted admin key access: full access from global store
    ant_key = await credential_store.get("system:anthropic_api_key") or ""
    or_key  = await credential_store.get("system:openrouter_api_key") or ""
    oai_key = await credential_store.get("system:openai_api_key") or ""
    return ant_key, or_key, oai_key


def _is_free_openrouter(m: dict) -> bool:
    """Return True if this OpenRouter model is free (pricing.prompt == "0")."""
    pricing = m.get("pricing", {})
    try:
        return float(pricing.get("prompt", "1")) == 0.0 and float(pricing.get("completion", "1")) == 0.0
    except (TypeError, ValueError):
        return False


async def get_available_models(
    user_id: str | None = None,
    is_admin: bool = True,
) -> tuple[list[str], str]:
    """
    Return (model_list, default_model).

    Always auto-builds from active providers:
      - Hardcoded Anthropic models if ANTHROPIC_API_KEY is set (and user has access)
      - All OpenRouter models (fetched + cached 1h) if OPENROUTER_API_KEY is set
        - Non-admin users with no own OR key are limited to free models only

    DEFAULT_CHAT_MODEL in .env sets the pre-selected default.
    """
    from ..config import settings
    from ..database import user_settings_store

    ant_key, or_key, oai_key = await _get_keys(user_id=user_id, is_admin=is_admin)

    # Determine access restrictions for non-admin users
    free_or_only = False
    if user_id and not is_admin:
        use_admin_keys = await user_settings_store.get(user_id, "use_admin_keys")
        if not use_admin_keys:
            own_ant = await user_settings_store.get(user_id, "anthropic_api_key")
            own_or  = await user_settings_store.get(user_id, "openrouter_api_key")
            if not own_ant:
                ant_key = ""  # block Anthropic unless they have their own key
            if not own_or and or_key:
                free_or_only = True

    models: list[str] = []
    if ant_key:
        models.extend(_ANTHROPIC_MODELS)
    if oai_key:
        models.extend(_OPENAI_MODELS)
    if or_key:
        raw = await _fetch_openrouter_raw(or_key)
        if free_or_only:
            raw = [m for m in raw if _is_free_openrouter(m)]
        models.extend(sorted(f"openrouter:{m['id']}" for m in raw))

    from ..database import credential_store
    if free_or_only:
        db_default = await credential_store.get("system:default_chat_model_free") \
                     or await credential_store.get("system:default_chat_model")
    else:
        db_default = await credential_store.get("system:default_chat_model")

    # Resolve default: DB override → .env → first available model
    candidate = db_default or settings.default_chat_model or (models[0] if models else "")
    # Ensure the candidate is actually in the model list
    default = candidate if candidate in models else (models[0] if models else "")
    return models, default


def get_or_output_modalities(bare_model_id: str) -> list[str]:
    """
    Return output_modalities for an OpenRouter model from the cached raw API data.
    Falls back to ["text"] if not found or cache is empty.
    Also detects known image-gen models by ID pattern as a fallback.
    """
    for m in _or_raw:
        if m.get("id") == bare_model_id:
            return m.get("architecture", {}).get("output_modalities") or ["text"]
    # Pattern fallback for when cache is cold or model isn't listed
    low = bare_model_id.lower()
    if any(p in low for p in ("-image", "/flux", "image-gen", "imagen")):
        return ["image", "text"]
    return ["text"]


async def get_capability_map(
    user_id: str | None = None,
    is_admin: bool = True,
) -> dict[str, dict]:
    """Return {model_id: {vision, tools, online}} for all available models."""
    info = await get_models_info(user_id=user_id, is_admin=is_admin)
    return {m["id"]: m.get("capabilities", {}) for m in info}


async def get_models_info(
    user_id: str | None = None,
    is_admin: bool = True,
) -> list[dict]:
    """
    Return rich metadata for all available models, filtered by user access tier.

    Anthropic entries use hardcoded info.
    OpenRouter entries are derived from the live API response.
    """
    from ..config import settings
    from ..database import user_settings_store

    ant_key, or_key, oai_key = await _get_keys(user_id=user_id, is_admin=is_admin)

    free_or_only = False
    if user_id and not is_admin:
        own_ant = await user_settings_store.get(user_id, "anthropic_api_key")
        own_or  = await user_settings_store.get(user_id, "openrouter_api_key")
        if not own_ant:
            ant_key = ""
        if not own_or and or_key:
            free_or_only = True

    results: list[dict] = []

    if ant_key:
        results.extend(_ANTHROPIC_MODEL_INFO)

    if oai_key:
        results.extend(_OPENAI_MODEL_INFO)

    if or_key:
        raw = await _fetch_openrouter_raw(or_key)
        if free_or_only:
            raw = [m for m in raw if _is_free_openrouter(m)]
        for m in raw:
            model_id = m.get("id", "")
            pricing = m.get("pricing", {})
            try:
                prompt_per_1m = float(pricing.get("prompt", 0)) * 1_000_000
            except (TypeError, ValueError):
                prompt_per_1m = None
            try:
                completion_per_1m = float(pricing.get("completion", 0)) * 1_000_000
            except (TypeError, ValueError):
                completion_per_1m = None

            arch = m.get("architecture", {})

            # Vision: OpenRouter returns either a list (new) or a modality string (old)
            input_modalities = arch.get("input_modalities") or []
            if not input_modalities:
                modality_str = arch.get("modality", "")
                input_part = modality_str.split("->")[0] if "->" in modality_str else modality_str
                input_modalities = [p.strip() for p in input_part.replace("+", " ").split() if p.strip()]

            # Tools: field may be named either way depending on API version
            supported_params = (
                m.get("supported_generation_parameters")
                or m.get("supported_parameters")
                or []
            )

            # Online: inherently-online models have "online" in their ID or name,
            # or belong to providers whose models are always web-connected
            name_lower = (m.get("name") or "").lower()
            online = (
                "online" in model_id
                or model_id.startswith("perplexity/")
                or "online" in name_lower
            )

            out_modalities = arch.get("output_modalities", ["text"])

            modality_display = arch.get("modality", "")
            if not modality_display and input_modalities:
                modality_display = "+".join(input_modalities) + "->" + "+".join(out_modalities)

            results.append({
                "id": f"openrouter:{model_id}",
                "provider": "openrouter",
                "bare_id": model_id,
                "name": m.get("name") or model_id,
                "context_length": m.get("context_length"),
                "description": m.get("description") or "",
                "capabilities": {
                    "vision": "image" in input_modalities,
                    "tools": "tools" in supported_params,
                    "online": online,
                    "image_gen": "image" in out_modalities,
                },
                "pricing": {
                    "prompt_per_1m": prompt_per_1m,
                    "completion_per_1m": completion_per_1m,
                },
                "architecture": {
                    "tokenizer": arch.get("tokenizer", ""),
                    "modality": modality_display,
                },
            })

    return results


def get_model_pricing(model_id: str) -> tuple[float | None, float | None]:
    """
    Return (prompt_per_1m, completion_per_1m) in USD for the given model ID.

    Uses only in-memory data (hardcoded Anthropic/OpenAI + cached OpenRouter raw).
    Returns (None, None) if pricing is unknown for this model.

    Handles multiple formats:
      "anthropic:claude-sonnet-5"  — prefixed (canonical)
      "claude-sonnet-5"            — bare Anthropic ID (stored by Anthropic provider)
      "openrouter:openai/gpt-4o"     — prefixed OpenRouter ID
      "openai/gpt-4o"                — bare OpenRouter ID (stored by some agents)
      "openai:gpt-4o"                — prefixed OpenAI ID
    """
    # Anthropic — match on full prefixed id or bare_id
    for m in _ANTHROPIC_MODEL_INFO:
        if m["id"] == model_id or m["bare_id"] == model_id:
            p = m["pricing"]
            return p["prompt_per_1m"], p["completion_per_1m"]
    # OpenAI — match on full prefixed id or bare_id
    for m in _OPENAI_MODEL_INFO:
        if m["id"] == model_id or m["bare_id"] == model_id:
            p = m["pricing"]
            return p["prompt_per_1m"], p["completion_per_1m"]
    # OpenRouter: strip "openrouter:" prefix if present, then look up in cached raw list
    or_bare = model_id[len("openrouter:"):] if model_id.startswith("openrouter:") else model_id
    for m in _or_raw:
        if m.get("id", "") == or_bare and not _is_free_openrouter(m):
            pricing = m.get("pricing", {})
            try:
                prompt = float(pricing.get("prompt", 0)) * 1_000_000
                completion = float(pricing.get("completion", 0)) * 1_000_000
                return prompt, completion
            except (TypeError, ValueError):
                return None, None
    return None, None


def compute_cost_usd(
    model_id: str,
    input_tokens: int,
    output_tokens: int,
) -> float | None:
    """Compute estimated cost in USD for a completed run. Returns None if pricing unknown."""
    prompt_per_1m, completion_per_1m = get_model_pricing(model_id)
    if prompt_per_1m is None or completion_per_1m is None:
        return None
    return (input_tokens / 1_000_000) * prompt_per_1m + (output_tokens / 1_000_000) * completion_per_1m


async def get_access_tier(
    user_id: str | None = None,
    is_admin: bool = True,
) -> dict:
    """Return access restriction flags for the given user."""
    if not user_id or is_admin:
        return {"anthropic_blocked": False, "openrouter_free_only": False, "openai_blocked": False}
    from ..database import user_settings_store, credential_store
    use_admin_keys = await user_settings_store.get(user_id, "use_admin_keys")
    if use_admin_keys:
        return {"anthropic_blocked": False, "openrouter_free_only": False, "openai_blocked": False}
    own_ant   = await user_settings_store.get(user_id, "anthropic_api_key")
    own_or    = await user_settings_store.get(user_id, "openrouter_api_key")
    global_or = await credential_store.get("system:openrouter_api_key")
    return {
        "anthropic_blocked": not bool(own_ant),
        "openrouter_free_only": not bool(own_or) and bool(global_or),
        "openai_blocked": True,  # Non-admins always need their own OpenAI key
    }


def invalidate_openrouter_cache() -> None:
    """Force a fresh fetch on the next call (e.g. after an API key change)."""
    global _or_cache_ts
    _or_cache_ts = 0.0
