"""Define API routes for status."""

import asyncio
import time
from typing import Any

from fastapi import APIRouter

from app.config.settings import settings
from app.domain.retrieval import RetrievedChunk
from app.services.llm.llm_factory import get_llm_client, get_llm_factory
from app.shared.bootstrap import (
    get_bm25_retriever,
    get_binary_store,
    get_conversation_store,
    get_document_query_service,
    get_embedding_provider,
    get_reranker,
    get_vector_index,
)
from app.shared.model_usage_tracker import get_model_usage_tracker

router = APIRouter(prefix="/status", tags=["系统状态"])

# ---------------------------------------------------------------------------
# Simple TTL cache for /stats (avoids O(N) doc scan on every request)
# ---------------------------------------------------------------------------
_stats_cache: dict[str, Any] = {}
_stats_cache_time: float = 0.0
_STATS_TTL_SECONDS: float = 10.0

# ---------------------------------------------------------------------------
# AI model roles surfaced on the Status page (Task: System Status AI models)
# ---------------------------------------------------------------------------
_MODEL_ROLES: dict[str, str] = {
    "main_llm": "主问答 LLM",
    "hyde_llm": "HyDE 查询增强",
    "embedding": "Embedding",
    "reranker": "Reranker",
}


@router.get("/stats")
async def get_stats():
    """Return document statistics (cached for 10 s)."""
    global _stats_cache, _stats_cache_time
    now = time.time()
    if _stats_cache and (now - _stats_cache_time) < _STATS_TTL_SECONDS:
        return _stats_cache

    documents = get_document_query_service().list_documents()
    indexed = sum(1 for d in documents if d.status.value == "indexed")
    failed = sum(1 for d in documents if d.status.value == "failed")
    _stats_cache = {
        "documents_total": len(documents),
        "documents_indexed": indexed,
        "documents_failed": failed,
        "chunks_total": sum(d.chunk_count for d in documents),
    }
    _stats_cache_time = now
    return _stats_cache


@router.get("/config")
async def get_config():
    """Return system configuration."""
    return {
        "embedding_model": settings.embedding_model,
        "embedding_dim": settings.embedding_dim,
        "embedding_base_url": settings.embedding_base_url,
        "milvus_collection": settings.milvus_collection,
        "parser_backend": settings.parser_backend,
        "chunk_backend": settings.chunk_backend,
        "artifact_prefix": settings.document_parse_artifact_prefix,
        "parser_failure_mode": settings.parser_failure_mode,
        "llm_provider": settings.llm_provider,
        "llm_model": settings.llm_model,
        "document_metadata_path": settings.document_metadata_path,
    }


@router.get("/milvus/health")
async def milvus_health():
    """Return Milvus health (kept for backwards compat)."""
    return get_vector_index().health()


@router.get("/health")
async def get_health():
    """Return aggregate health of all backend services."""
    # --- Milvus ---
    try:
        milvus_info = get_vector_index().health()
        milvus_status = "ok" if milvus_info.get("connected") else "error"
    except Exception as exc:  # noqa: BLE001
        milvus_info = {}
        milvus_status = "error"
        milvus_info["error"] = str(exc)

    # --- MinIO ---
    try:
        minio_connected = get_binary_store().client.connected
        minio_status = "ok" if minio_connected else "error"
    except Exception:  # noqa: BLE001
        minio_status = "error"
        minio_connected = False

    # --- BM25 ---
    bm25 = get_bm25_retriever()

    # --- Sessions ---
    try:
        session_count = len(get_conversation_store().list_sessions())
    except Exception:  # noqa: BLE001
        session_count = 0

    return {
        "milvus": {"status": milvus_status, **milvus_info},
        "minio": {"status": minio_status, "connected": minio_connected},
        "bm25": {"available": bm25 is not None},
        "reranker": {
            "enabled": settings.reranker_enabled,
            "model": settings.reranker_model if settings.reranker_enabled else None,
        },
        "sessions": {
            "active": session_count,
            "max": settings.session_max_sessions,
        },
    }


def _normalize_llm_provider(raw_provider: str) -> str:
    """Normalize a raw LLM_PROVIDER/HYDE_LLM_PROVIDER settings string to the
    canonical LLMProvider enum value, the SAME way LLMFactory.create() does.

    TrackedLLMClient.chat() (tracked_client.py) always records usage under
    `self._inner.config.provider.value` — the NORMALIZED enum value produced by
    LLMFactory._parse_provider() — never the raw string a caller passed to
    get_llm_client(). Reusing that same normalization here (instead of
    duplicating the alias table) guarantees the tracker key this route reads
    always agrees with the key TrackedLLMClient wrote, even when the raw
    settings value is a non-canonical alias (e.g. "deepseek-v3") or different
    casing. Falls back to the raw string, unchanged, if it does not match any
    known provider/alias, so this passive status endpoint still renders
    (as "never_called") instead of raising on a misconfigured provider string.
    """
    try:
        return get_llm_factory()._parse_provider(raw_provider).value
    except ValueError:
        return raw_provider


def _resolve_role_provider_model(role: str) -> tuple[str, str]:
    """Return the (provider, model) pair currently configured for one AI model role.

    For "hyde_llm" this mirrors the exact fallback logic already used in
    hyde_expander.py (settings.hyde_llm_provider or settings.llm_provider, same
    for model) so tracker lookups here always match what TrackedLLMClient
    recorded when HyDE actually ran.
    """
    if role == "main_llm":
        return _normalize_llm_provider(settings.llm_provider), settings.llm_model
    if role == "hyde_llm":
        return (
            _normalize_llm_provider(settings.hyde_llm_provider or settings.llm_provider),
            settings.hyde_llm_model or settings.llm_model,
        )
    if role == "embedding":
        return "embedding", settings.embedding_model
    if role == "reranker":
        return "reranker", settings.reranker_model
    raise ValueError(f"unknown model role: {role}")  # pragma: no cover - internal roles are fixed


def _build_model_status(role: str) -> dict[str, Any]:
    """Build one /status/models row for the given role from tracker data + live settings."""
    provider, model = _resolve_role_provider_model(role)
    entry = get_model_usage_tracker().get(provider, model)

    main_provider, main_model = _resolve_role_provider_model("main_llm")
    shares_usage_with = (
        "main_llm" if role != "main_llm" and (provider, model) == (main_provider, main_model) else None
    )

    enabled = True
    status = entry.status if entry else "never_called"
    if role == "reranker":
        enabled = settings.reranker_enabled
        if not enabled:
            # Config always wins: report "disabled" even if the reranker was
            # enabled and called successfully earlier in this process's life.
            status = "disabled"
    elif role == "hyde_llm":
        enabled = settings.hyde_enabled
        if not enabled:
            # Same "config always wins" override as the reranker branch above:
            # report "disabled" even if HyDE ran successfully before being
            # turned off in settings during this process's life.
            status = "disabled"

    return {
        "role": role,
        "role_label": _MODEL_ROLES[role],
        "provider": provider,
        "model": model,
        "enabled": enabled,
        "status": status,
        "total_tokens": entry.total_tokens if entry else 0,
        "call_count_ok": entry.call_count_ok if entry else 0,
        "call_count_error": entry.call_count_error if entry else 0,
        "last_called_at": entry.last_called_at.isoformat() if entry and entry.last_called_at else None,
        "last_latency_ms": entry.last_latency_ms if entry else None,
        "last_error": entry.last_error if entry else None,
        "shares_usage_with": shares_usage_with,
    }


@router.get("/models")
async def get_model_statuses():
    """Return connection status + cumulative token usage for all 4 tracked AI model roles.

    Passive: reads tracker state + settings only, makes no outbound network calls.
    """
    return {"models": [_build_model_status(role) for role in _MODEL_ROLES]}


async def _ping_main_or_hyde(role: str) -> None:
    """Send one minimal chat completion to the LLM configured for `role`.

    Skipped entirely for "hyde_llm" when settings.hyde_enabled is False,
    mirroring _ping_reranker()'s disabled-skip pattern: when HyDE is turned
    off (or reuses the main LLM, the default), issuing this ping would just be
    a redundant duplicate chat call against the same model for no benefit.
    "main_llm" is always pinged regardless of this check.
    """
    if role == "hyde_llm" and not settings.hyde_enabled:
        return
    provider, model = _resolve_role_provider_model(role)
    try:
        client = get_llm_client(provider=provider, model=model)
    except Exception as exc:  # noqa: BLE001 - record, then re-raise so gather() still isolates this ping
        # get_llm_client() can fail before any TrackedLLMClient exists to
        # record the outcome itself (e.g. missing API key, unsupported
        # provider string), so record the failure here directly, otherwise it
        # would be invisible on the /status/models page afterward.
        get_model_usage_tracker().record(provider=provider, model=model, success=False, error=str(exc))
        raise
    await asyncio.to_thread(client.chat, [{"role": "user", "content": "ping"}], max_tokens=1)


async def _ping_embedding() -> None:
    """Send one minimal embedding request."""
    await asyncio.to_thread(get_embedding_provider().embed_query, "ping")


async def _ping_reranker() -> None:
    """Send one minimal rerank request, only when the reranker is enabled."""
    reranker = get_reranker()
    if reranker is None:
        return
    # Minimal single-chunk probe — real content doesn't matter, only round-trip success.
    placeholder = RetrievedChunk(chunk_id="ping", doc_id="ping", doc_title="ping", text="ping", score=0.0)
    await asyncio.to_thread(reranker.rerank, "ping", [placeholder], 1)


@router.post("/models/ping")
async def ping_model_connections():
    """Actively test each configured model with a minimal request, then return fresh statuses.

    Each ping is isolated with return_exceptions=True so one model timing out
    or erroring does not prevent the other three from completing and being
    reported. Failures are still visible afterwards via _build_model_status()
    because the underlying clients record their own outcome into the tracker.
    """
    tasks = [
        _ping_main_or_hyde("main_llm"),
        _ping_main_or_hyde("hyde_llm"),
        _ping_embedding(),
        _ping_reranker(),
    ]
    await asyncio.gather(*tasks, return_exceptions=True)
    return {"models": [_build_model_status(role) for role in _MODEL_ROLES]}
