"""Provider selection and fallback logic for assistant AI calls."""

from __future__ import annotations

import hashlib
import logging
import os
from dataclasses import dataclass
from threading import Lock
from typing import Any

from app.core.config import settings
from app.services import runtime_config
from app.services.ai.providers import (
    AIProvider,
    AIProviderConfigError,
    AIProviderResponseError,
    AIProviderUnavailableError,
    GeminiProvider,
    OllamaProvider,
)
from app.services.ai.providers.ollama_provider import DEFAULT_OLLAMA_MODEL

LOGGER = logging.getLogger("sentineledge.ai.registry")

SUPPORTED_PROVIDER_IDS = ("gemini", "ollama")
DEFAULT_GEMINI_MODEL = "gemini-2.5-flash"
DEFAULT_OLLAMA_BASE_URL = "http://localhost:11434"

_RUNTIME_VERSION_LOCK = Lock()
_RUNTIME_VERSION = 0

_PROVIDER_CACHE_LOCK = Lock()
_PROVIDER_CACHE: dict[tuple[str, int, str, str], AIProvider] = {}


@dataclass(frozen=True, slots=True)
class ProviderSelection:
    """Resolved provider selection result."""

    selected_provider: str
    effective_provider: str
    fallback_used: bool
    provider: AIProvider


@dataclass(frozen=True, slots=True)
class RuntimeAIValues:
    """Canonical runtime AI settings with source metadata."""

    provider: str
    provider_source: str
    model: str
    model_source: str
    api_key: str
    api_key_source: str
    api_key_present: bool
    gemini_api_key: str
    gemini_api_key_source: str
    gemini_model: str
    gemini_model_source: str
    allow_fallback: bool
    allow_fallback_source: str
    ollama_model: str
    ollama_model_source: str
    ollama_base_url: str
    ollama_base_url_source: str
    version: int

    @property
    def selected_provider(self) -> str:
        """Backward-compatible alias."""
        return self.provider

    @property
    def selected_provider_source(self) -> str:
        """Backward-compatible alias."""
        return self.provider_source


def _normalize_provider_id(value: object | None) -> str:
    provider_id = str(value or "").strip().lower()
    if provider_id in SUPPORTED_PROVIDER_IDS:
        return provider_id
    return "gemini"


def invalidate_runtime_cache(*, reason: str = "runtime config update") -> int:
    """Bump provider runtime version and invalidate provider client cache."""
    global _RUNTIME_VERSION
    with _RUNTIME_VERSION_LOCK:
        _RUNTIME_VERSION += 1
        version = _RUNTIME_VERSION
    with _PROVIDER_CACHE_LOCK:
        _PROVIDER_CACHE.clear()
    LOGGER.debug("AI runtime invalidated version=%s reason=%s", version, reason)
    return version


def _runtime_version() -> int:
    with _RUNTIME_VERSION_LOCK:
        return _RUNTIME_VERSION


def _env_present(name: str) -> bool:
    raw = os.getenv(name)
    return raw is not None and bool(str(raw).strip())


def resolve_ai_config() -> RuntimeAIValues:
    """Resolve canonical AI config with precedence: UI config -> env -> defaults."""
    current = runtime_config.get_runtime_config()

    provider_from_ui = str(getattr(current, "ai_provider", "") or "").strip().lower() if current is not None else ""
    if provider_from_ui:
        selected_provider = _normalize_provider_id(provider_from_ui)
        selected_provider_source = "ui"
    else:
        selected_provider = _normalize_provider_id(settings.ai_provider)
        selected_provider_source = "env" if _env_present("AI_PROVIDER") else "default"

    gemini_key_from_ui = getattr(current, "gemini_api_key", None) if current is not None else None
    if gemini_key_from_ui is not None:
        gemini_api_key = str(gemini_key_from_ui or "").strip()
        gemini_api_key_source = "ui" if gemini_api_key else "none"
    else:
        gemini_api_key = str(settings.gemini_api_key or "").strip()
        if gemini_api_key and _env_present("GEMINI_API_KEY"):
            gemini_api_key_source = "env"
        elif gemini_api_key:
            gemini_api_key_source = "default"
        else:
            gemini_api_key_source = "none"

    gemini_model_from_ui = str(getattr(current, "gemini_model", "") or "").strip() if current is not None else ""
    if gemini_model_from_ui:
        gemini_model = gemini_model_from_ui
        gemini_model_source = "ui"
    else:
        gemini_model = str(settings.gemini_model or DEFAULT_GEMINI_MODEL).strip() or DEFAULT_GEMINI_MODEL
        gemini_model_source = "env" if _env_present("GEMINI_MODEL") else "default"

    if current is not None:
        allow_fallback = bool(getattr(current, "allow_fallback", False))
        allow_fallback_source = "ui"
    else:
        allow_fallback = bool(settings.ai_allow_fallback)
        allow_fallback_source = "env" if _env_present("AI_ALLOW_FALLBACK") else "default"

    ollama_model_from_ui = str(getattr(current, "ollama_model", "") or "").strip() if current is not None else ""
    if ollama_model_from_ui:
        ollama_model = ollama_model_from_ui
        ollama_model_source = "ui"
    else:
        ollama_model = str(settings.ollama_model or DEFAULT_OLLAMA_MODEL).strip() or DEFAULT_OLLAMA_MODEL
        ollama_model_source = "env" if _env_present("OLLAMA_MODEL") else "default"

    ollama_base_url = str(settings.ollama_base_url or DEFAULT_OLLAMA_BASE_URL).strip() or DEFAULT_OLLAMA_BASE_URL
    ollama_base_url_source = "env" if _env_present("OLLAMA_BASE_URL") else "default"

    resolved_model = gemini_model if selected_provider == "gemini" else ollama_model
    resolved_model_source = gemini_model_source if selected_provider == "gemini" else ollama_model_source
    resolved_api_key = gemini_api_key if selected_provider == "gemini" else ""
    resolved_api_key_source = gemini_api_key_source if selected_provider == "gemini" else "none"

    values = RuntimeAIValues(
        provider=selected_provider,
        provider_source=selected_provider_source,
        model=resolved_model,
        model_source=resolved_model_source,
        api_key=resolved_api_key,
        api_key_source=resolved_api_key_source,
        api_key_present=bool(resolved_api_key),
        gemini_api_key=gemini_api_key,
        gemini_api_key_source=gemini_api_key_source,
        gemini_model=gemini_model,
        gemini_model_source=gemini_model_source,
        allow_fallback=allow_fallback,
        allow_fallback_source=allow_fallback_source,
        ollama_model=ollama_model,
        ollama_model_source=ollama_model_source,
        ollama_base_url=ollama_base_url,
        ollama_base_url_source=ollama_base_url_source,
        version=_runtime_version(),
    )
    LOGGER.debug(
        "AI config resolved provider=%s source=%s model=%s model_source=%s api_key_present=%s key_source=%s cfg_version=%s",
        values.provider,
        values.provider_source,
        values.model,
        values.model_source,
        values.api_key_present,
        values.api_key_source,
        values.version,
    )
    return values


def resolve_runtime_ai_values() -> RuntimeAIValues:
    """Backward-compatible alias for canonical AI config resolver."""
    return resolve_ai_config()


def _runtime_ai_values() -> RuntimeAIValues:
    """Backward-compatible alias for canonical runtime AI config resolution."""
    return resolve_ai_config()


def _gemini_configured(values: RuntimeAIValues) -> bool:
    return bool(str(values.gemini_api_key or "").strip())


def _provider_cache_key(provider_id: str, values: RuntimeAIValues) -> tuple[str, int, str, str]:
    """Build cache key without storing raw API key bytes."""
    if provider_id == "gemini":
        key_digest = hashlib.sha256(values.gemini_api_key.encode("utf-8")).hexdigest() if values.gemini_api_key else "no-key"
        return ("gemini", values.version, values.gemini_model, key_digest)
    return ("ollama", values.version, values.ollama_model, values.ollama_base_url)


def _log_provider_selection(
    *,
    values: RuntimeAIValues,
    selected: str,
    effective: str,
    fallback_used: bool,
    available: bool | None = None,
) -> None:
    model = values.gemini_model if effective == "gemini" else values.ollama_model
    model_source = values.gemini_model_source if effective == "gemini" else values.ollama_model_source
    LOGGER.debug(
        "AI provider selected=%s selected_source=%s effective=%s model=%s model_source=%s fallback=%s available=%s cfg_version=%s",
        selected,
        values.selected_provider_source,
        effective,
        model,
        model_source,
        fallback_used,
        available,
        values.version,
    )


def build_provider(provider_id: str | None = None, *, values: RuntimeAIValues | None = None) -> AIProvider:
    """Instantiate provider adapter from resolved runtime settings."""
    resolved = values or resolve_ai_config()
    selected = _normalize_provider_id(provider_id or resolved.selected_provider)
    cache_key = _provider_cache_key(selected, resolved)

    with _PROVIDER_CACHE_LOCK:
        cached = _PROVIDER_CACHE.get(cache_key)
    if cached is not None:
        return cached

    if selected == "ollama":
        provider: AIProvider = OllamaProvider(
            model=str(resolved.ollama_model),
            base_url=str(resolved.ollama_base_url),
        )
    else:
        provider = GeminiProvider(
            api_key=str(resolved.gemini_api_key),
            model=str(resolved.gemini_model),
        )

    with _PROVIDER_CACHE_LOCK:
        _PROVIDER_CACHE[cache_key] = provider
    return provider


def resolve_generation_provider() -> ProviderSelection:
    """Resolve effective provider, applying fallback policy when needed."""
    values = resolve_ai_config()
    selected = str(_normalize_provider_id(values.selected_provider))
    provider = build_provider(selected, values=values)

    if selected == "gemini":
        if not _gemini_configured(values):
            raise AIProviderConfigError("Gemini not configured: missing API key. Open Settings.")
        _log_provider_selection(values=values, selected=selected, effective="gemini", fallback_used=False, available=True)
        return ProviderSelection(selected_provider=selected, effective_provider="gemini", fallback_used=False, provider=provider)

    ollama_available = provider.health_check()
    if ollama_available:
        _log_provider_selection(values=values, selected=selected, effective="ollama", fallback_used=False, available=True)
        return ProviderSelection(selected_provider=selected, effective_provider="ollama", fallback_used=False, provider=provider)

    LOGGER.warning(
        "AI provider unavailable selected=%s selected_source=%s model=%s model_source=%s cfg_version=%s",
        selected,
        values.selected_provider_source,
        values.ollama_model,
        values.ollama_model_source,
        values.version,
    )
    if values.allow_fallback and _gemini_configured(values):
        fallback = build_provider("gemini", values=values)
        _log_provider_selection(values=values, selected=selected, effective="gemini", fallback_used=True, available=True)
        return ProviderSelection(selected_provider=selected, effective_provider="gemini", fallback_used=True, provider=fallback)

    raise AIProviderUnavailableError("Selected local AI provider is not available")


def generate_text(prompt: str, context: dict[str, Any] | None = None) -> str:
    """Generate text using resolved provider with controlled fallback behavior."""
    selection = resolve_generation_provider()
    try:
        LOGGER.debug(
            "AI generate request path=app.services.ai.provider_registry.generate_text provider=%s effective=%s prompt_len=%s context_keys=%s",
            selection.selected_provider,
            selection.effective_provider,
            len(str(prompt or "")),
            sorted(str(key) for key in (context or {}).keys()),
        )
        return selection.provider.generate(prompt=prompt, context=context)
    except AIProviderUnavailableError as exc:
        LOGGER.warning(
            "AI generate unavailable provider=%s effective=%s error_class=%s error=%s",
            selection.selected_provider,
            selection.effective_provider,
            exc.__class__.__name__,
            str(exc),
        )
        values = resolve_ai_config()
        if (
            selection.selected_provider == "ollama"
            and selection.effective_provider == "ollama"
            and values.allow_fallback
            and _gemini_configured(values)
        ):
            LOGGER.warning(
                "AI provider runtime failure selected=%s trying_fallback=gemini cfg_version=%s",
                selection.selected_provider,
                values.version,
            )
            fallback = build_provider("gemini", values=values)
            return fallback.generate(prompt=prompt, context=context)
        raise
    except (AIProviderConfigError, AIProviderResponseError) as exc:
        LOGGER.warning(
            "AI generate failure provider=%s effective=%s error_class=%s error=%s",
            selection.selected_provider,
            selection.effective_provider,
            exc.__class__.__name__,
            str(exc),
        )
        raise


def selected_provider_health() -> dict[str, Any]:
    """Return selected provider id and availability."""
    values = resolve_ai_config()
    selected = str(values.selected_provider)
    provider = build_provider(selected, values=values)
    if selected == "gemini":
        available = _gemini_configured(values)
    else:
        available = provider.health_check()
    return {
        "selected": selected,
        "available": bool(available),
        "source": values.selected_provider_source,
        "model": values.model,
        "model_source": values.model_source,
        "api_key_present": values.api_key_present,
    }


def ai_config_health() -> dict[str, Any]:
    """Return resolved provider config health summary without secrets."""
    values = resolve_ai_config()
    provider_ready = bool(values.api_key_present) if values.provider == "gemini" else build_provider(
        "ollama",
        values=values,
    ).health_check()
    return {
        "resolved_provider": values.provider,
        "resolved_model": values.model,
        "provider_source": values.provider_source,
        "model_source": values.model_source,
        "api_key_present": values.api_key_present,
        "api_key_source": values.api_key_source,
        "provider_ready": bool(provider_ready),
        "config_version": values.version,
    }


def test_provider(
    *,
    provider_id: str | None = None,
    gemini_api_key: str | None = None,
    gemini_model: str | None = None,
    ollama_model: str | None = None,
) -> dict[str, Any]:
    """Run explicit provider health check for configuration UI."""
    values = resolve_ai_config()
    selected = _normalize_provider_id(provider_id or values.selected_provider)

    if selected == "gemini":
        key = str(gemini_api_key or values.gemini_api_key or "").strip()
        model = str(gemini_model or values.gemini_model or DEFAULT_GEMINI_MODEL).strip() or DEFAULT_GEMINI_MODEL
        if not key:
            return {
                "provider": "gemini",
                "ok": False,
                "detail": "Gemini not configured: missing API key. Open Settings.",
            }

        provider = GeminiProvider(api_key=key, model=model)
        LOGGER.debug(
            "AI provider test selected=gemini model=%s model_source=%s key_source=%s api_key_present=%s cfg_version=%s",
            model,
            "ui" if gemini_model else values.gemini_model_source,
            "ui" if gemini_api_key is not None else values.gemini_api_key_source,
            bool(key),
            values.version,
        )
        try:
            provider.generate(prompt="Reply with exactly: ok")
        except (AIProviderConfigError, AIProviderUnavailableError, AIProviderResponseError) as exc:
            return {
                "provider": "gemini",
                "ok": False,
                "detail": str(exc) or f"Gemini model '{model}' not reachable.",
            }
        return {
            "provider": "gemini",
            "ok": True,
            "detail": f"Gemini model '{model}' reachable.",
        }

    model = str(ollama_model or values.ollama_model or DEFAULT_OLLAMA_MODEL).strip() or DEFAULT_OLLAMA_MODEL
    provider = OllamaProvider(model=model, base_url=str(values.ollama_base_url))
    LOGGER.debug(
        "AI provider test selected=ollama model=%s model_source=%s cfg_version=%s",
        model,
        "ui" if ollama_model else values.ollama_model_source,
        values.version,
    )
    ok = provider.health_check()
    return {
        "provider": "ollama",
        "ok": bool(ok),
        "detail": f"Ollama model '{model}' is running." if ok else f"Ollama model '{model}' not detected.",
    }


__all__ = [
    "AIProviderConfigError",
    "AIProviderResponseError",
    "AIProviderUnavailableError",
    "ProviderSelection",
    "build_provider",
    "generate_text",
    "resolve_generation_provider",
    "selected_provider_health",
    "test_provider",
    "invalidate_runtime_cache",
    "resolve_ai_config",
    "resolve_runtime_ai_values",
    "ai_config_health",
]
