"""EB1-preview model resolver for one-pass routing."""

from __future__ import annotations

import logging
import threading
import time
from dataclasses import dataclass, field
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from ember.eb1_gnn.routing import EmbeddedRouter

logger = logging.getLogger(__name__)


@dataclass(frozen=True)
class Eb1RoutingResult:
    """Result of eb1-preview routing decision."""

    original_model: str
    resolved_model: str
    tier: str
    confidence: float
    policy: str
    routing_latency_ms: float
    metadata: dict[str, object] = field(default_factory=dict)

    def to_dict(self) -> dict[str, object]:
        """Return a serializable dict for logging and debugging."""
        return {
            "original_model": self.original_model,
            "resolved_model": self.resolved_model,
            "tier": self.tier,
            "confidence": self.confidence,
            "policy": self.policy,
            "routing_latency_ms": self.routing_latency_ms,
            "metadata": self.metadata,
        }


def is_eb1_preview_model(model: str) -> bool:
    """Return True if model is an eb1-*-preview variant requiring GNN routing.

    SWE variants (eb1-swe-*) are excluded — they use keyword-only routing
    via ``resolve_eb1_swe_model`` instead.
    """
    name = model.lower()
    if name.startswith("takumi/"):
        name = name.split("/", 1)[1]
    if name.startswith("eb1-swe-"):
        return False
    return name.startswith("eb1-") and name.endswith("-preview")


def _extract_policy_from_model(model: str) -> str:
    """Extract the routing policy from an eb1-preview model name."""
    name = model.lower()
    if name.startswith("takumi/"):
        name = name.split("/", 1)[1]
    return name


_embedded_router_lock = threading.Lock()
_embedded_router: "EmbeddedRouter | None" = None
_embedded_router_error: Exception | None = None


def _get_embedded_router() -> "EmbeddedRouter | None":
    """Return the embedded router singleton, or None if unavailable."""
    global _embedded_router, _embedded_router_error

    if _embedded_router is not None:
        return _embedded_router
    if _embedded_router_error is not None:
        return None

    with _embedded_router_lock:
        if _embedded_router is not None:
            return _embedded_router
        if _embedded_router_error is not None:
            return None

        try:
            from ember.eb1_gnn.handlers import get_embedded_router

            router = get_embedded_router()
            if router is None:
                logger.info("Embedded router not available (not configured)")
                return None
            _embedded_router = router
            logger.info(
                "Embedded router loaded successfully: embed_dim=%d",
                router.embed_dim,
            )
            return router
        except ImportError as e:
            _embedded_router_error = e
            logger.debug("Embedded router not available: %s", e)
            return None
        except Exception as e:
            _embedded_router_error = e
            logger.warning("Failed to load embedded router: %s", e)
            return None


def _route_with_embedded_router(
    router: "EmbeddedRouter",
    query: str,
    policy: str,
) -> Eb1RoutingResult:
    """Route using the embedded router."""
    start = time.perf_counter()
    result = router.route(query)
    latency_ms = (time.perf_counter() - start) * 1000

    signals = result.signals
    confidence = signals.max_probability if signals else 0.0
    metadata: dict[str, object] = {
        "top1_model": signals.top1_model if signals else None,
        "max_probability": signals.max_probability if signals else 0.0,
        "model_probabilities": dict(signals.model_probabilities) if signals else {},
        "embedding_latency_ms": result.embedding_latency_ms,
        "classification_latency_ms": result.classification_latency_ms,
        "rationale": result.rationale,
    }

    return Eb1RoutingResult(
        original_model=f"takumi/{policy}",
        resolved_model=result.selected_model,
        tier=result.tier.name,
        confidence=confidence,
        policy=policy,
        routing_latency_ms=latency_ms,
        metadata=metadata,
    )


def _route_with_gnn_server(
    model: str,
    query: str,
    policy: str,
) -> Eb1RoutingResult:
    """Route by calling the external EB1-GNN server (legacy fallback)."""
    from ember.eb1_gnn.client import get_eb1_client, is_gnn_configured

    if not is_gnn_configured():
        raise ValueError(
            "EB1 routing not available: neither embedded router nor GNN server configured"
        )

    start = time.perf_counter()
    client = get_eb1_client()

    # Use the prefix-stripped policy name — the GNN server does not
    # recognise the ``takumi/`` namespace prefix.
    response = client.chat.completions.create(
        model=policy,
        messages=[{"role": "user", "content": query}],
        max_tokens=1,
    )

    latency_ms = (time.perf_counter() - start) * 1000
    resolved_model = response.model if response.model else policy

    return Eb1RoutingResult(
        original_model=model,
        resolved_model=resolved_model,
        tier="UNKNOWN",  # Server doesn't expose tier info directly
        confidence=0.0,
        policy=policy,
        routing_latency_ms=latency_ms,
        metadata={
            "routing_mode": "gnn_server",
            "server_model": response.model,
        },
    )


class Eb1RoutingUnavailable(Exception):
    """Raised when eb1-preview routing is requested but no router is available."""

    def __init__(self, model: str) -> None:
        self.model = model
        super().__init__(
            f"EB1 routing unavailable for model={model}: "
            "neither embedded router nor GNN server is configured"
        )


def resolve_eb1_preview_model(
    model: str,
    query: str,
    policy: str | None = None,
) -> Eb1RoutingResult:
    """Resolve eb1-preview to underlying model using the best available router.

    Raises:
        Eb1RoutingUnavailable: When no router (embedded or GNN server) is configured.
    """
    effective_policy = policy or _extract_policy_from_model(model)

    router = _get_embedded_router()
    if router is not None:
        return _route_with_embedded_router(router, query, effective_policy)

    try:
        from ember.eb1_gnn.client import is_gnn_configured

        if is_gnn_configured():
            return _route_with_gnn_server(model, query, effective_policy)
    except ImportError:
        pass

    raise Eb1RoutingUnavailable(model)


def is_eb1_swe_model(model: str) -> bool:
    """Return True if model is an eb1-swe-* variant (keyword-routed)."""
    name = model.lower()
    if name.startswith("takumi/"):
        name = name.split("/", 1)[1]
    return name.startswith("eb1-swe-")


# Mapping from eb1-swe model name patterns to SweVariant strings.
_SWE_VARIANT_MAP: dict[str, str] = {
    "eb1-swe-fast-preview": "fast",
    "eb1-swe-pro-preview": "pro",
    "eb1-swe-preview": "balanced",
}


def _extract_swe_variant(model: str) -> str:
    """Extract the SWE variant from an eb1-swe-* model name.

    Returns:
        One of "balanced", "fast", or "pro".
    """
    name = model.lower()
    if name.startswith("takumi/"):
        name = name.split("/", 1)[1]
    # Match longest prefix first (fast/pro before plain)
    for prefix, variant in _SWE_VARIANT_MAP.items():
        if name.startswith(prefix):
            return variant
    return "balanced"


def resolve_eb1_swe_model(model: str, query: str) -> Eb1RoutingResult:
    """Resolve an eb1-swe-* model to a concrete model via keyword classification.

    Uses ``classify_complexity`` from ``eb1_swe`` to determine complexity,
    then maps to the appropriate concrete model for the variant.

    **Gateway limitation**: For the pro variant's ensemble mapping
    (complex/architectural), this resolver collapses ``"ensemble"`` to the
    judge model (``PRO_ENSEMBLE_JUDGE``) because the HTTP gateway cannot
    orchestrate multi-model ensembles inline.  In-process callers (e.g.
    ``Eb1EnsembleProvider``) should detect the pro+complex/architectural case
    *before* calling this resolver and run the ensemble via ``run_plan()``
    directly.  See ``Eb1EnsembleProvider._run_swe_pro_ensemble()``.

    Args:
        model: The eb1-swe-* model name (e.g. "eb1-swe-preview").
        query: The user query text for classification.

    Returns:
        Eb1RoutingResult with the resolved concrete model.
    """
    from keiro.ensemble.eb1_swe import (
        COMPLEXITY_MODEL_MAP,
        FAST_COMPLEXITY_MODEL_MAP,
        PRO_COMPLEXITY_MODEL_MAP,
        PRO_ENSEMBLE_JUDGE,
        _resolve_first_available,
        classify_complexity,
    )

    start = time.perf_counter()

    variant = _extract_swe_variant(model)
    complexity, signals = classify_complexity(query)

    # Select the variant-appropriate model map
    if variant == "fast":
        model_map = FAST_COMPLEXITY_MODEL_MAP
    elif variant == "pro":
        model_map = PRO_COMPLEXITY_MODEL_MAP
    else:
        model_map = COMPLEXITY_MODEL_MAP

    candidates = model_map.get(
        complexity,
        model_map.get("standard", ("gpt-5.2",)),
    )
    resolved = _resolve_first_available(candidates)

    # Gateway cannot orchestrate ensembles inline; resolve to judge model
    if resolved == "ensemble":
        resolved = PRO_ENSEMBLE_JUDGE

    latency_ms = (time.perf_counter() - start) * 1000

    return Eb1RoutingResult(
        original_model=model,
        resolved_model=resolved,
        tier=complexity,
        confidence=1.0,
        policy=f"eb1-swe-{variant}",
        routing_latency_ms=latency_ms,
        metadata={
            "routing_mode": "swe_keyword",
            "variant": variant,
            "complexity": complexity,
            "signals": list(signals),
        },
    )


def _extract_query_for_routing(messages: list[object]) -> str:
    """Return the last user message content, or empty string if none.

    Handles both dict messages and message objects with role/content attributes.
    Content can be a plain string or a list of content parts (Responses API
    format where strings are normalized to ``[{"type": "input_text", "text": ...}]``).
    """
    for msg in reversed(messages):
        if isinstance(msg, dict):
            role = msg.get("role")
            content = msg.get("content")
        else:
            role = getattr(msg, "role", None)
            content = getattr(msg, "content", None)
        if role == "user":
            if isinstance(content, str) and content.strip():
                return content
            if isinstance(content, list):
                text_parts: list[str] = []
                for part in content:
                    if isinstance(part, str):
                        text_parts.append(part)
                    elif isinstance(part, dict):
                        text = part.get("text")
                        if isinstance(text, str):
                            text_parts.append(text)
                    else:
                        text = getattr(part, "text", None)
                        if isinstance(text, str):
                            text_parts.append(text)
                combined = " ".join(text_parts).strip()
                if combined:
                    return combined
    return ""


__all__ = [
    "Eb1RoutingResult",
    "Eb1RoutingUnavailable",
    "is_eb1_preview_model",
    "is_eb1_swe_model",
    "resolve_eb1_preview_model",
    "resolve_eb1_swe_model",
    "_extract_query_for_routing",
]
