"""Model lifecycle management — shared by TUI, sidecar, and future server.

Provides ``list_models()`` for discovery and ``switch_model()`` for
hot-swapping the active model with full credential checking, settings
rollback on failure, and agent recompilation.

Config imports are top-level (lightweight, no Rich dependency).
Only ``create_icarus_agent`` is deferred (heavy LangGraph machinery).
"""

from __future__ import annotations

import logging
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any

from icarus.config.app_config import create_model, detect_provider, settings
from icarus.config.model_config import (
    ModelConfig,
    ModelConfigError,
    ModelSpec,
    clear_default_model,
    get_available_models,
    get_credential_env_var,
    has_provider_credentials,
    save_default_model,
    save_recent_model,
)

if TYPE_CHECKING:
    from icarus.runtime.context import UserContext
    from langgraph.checkpoint.base import BaseCheckpointSaver

logger = logging.getLogger(__name__)

__all__ = [
    "DefaultResult",
    "ModelsInfo",
    "SwitchResult",
    "clear_default",
    "list_models",
    "set_default",
    "switch_model",
]


@dataclass
class ModelsInfo:
    """Available models and current state."""

    models: list[dict[str, str]]
    """Each entry: ``{"provider": "anthropic", "model": "claude-..."}``."""

    current: str | None = None
    """Currently active model as ``"provider:model"``, or ``None``."""

    default: str | None = None
    """Effective default — explicit or recent fallback."""

    default_explicit: str | None = None
    """Explicitly configured default only (no recent fallback)."""


@dataclass
class SwitchResult:
    """Outcome of a ``switch_model()`` attempt."""

    ok: bool
    display_name: str = ""
    graph: Any = None
    backend: Any = None
    model: Any = None
    error: str = ""
    already_active: bool = False
    preference_saved: bool = True


def list_models() -> ModelsInfo:
    """Return available models, current model, and default.

    Combines config-file providers with credentialed langchain providers.
    """
    config = ModelConfig.load()
    available = get_available_models()

    models = [
        {"provider": provider, "model": model}
        for provider, model_list in sorted(available.items())
        for model in model_list
    ]

    current = None
    if settings.model_name and settings.model_provider:
        current = f"{settings.model_provider}:{settings.model_name}"

    # Only report a default if that model is actually available — prevents
    # the picker from showing an unreachable Ollama model as "selected".
    all_specs = {f"{m['provider']}:{m['model']}" for m in models}
    default = None
    for candidate in (config.default_model, config.recent_model):
        if candidate and candidate in all_specs:
            default = candidate
            break
    if default is None and models:
        default = f"{models[0]['provider']}:{models[0]['model']}"

    return ModelsInfo(
        models=models,
        current=current,
        default=default,
        default_explicit=config.default_model,
    )


@dataclass
class DefaultResult:
    """Outcome of a ``set_default()`` or ``clear_default()`` attempt."""

    ok: bool
    display_name: str = ""
    error: str = ""


def _normalize_spec(spec: str) -> str:
    """Normalize a model spec — strip leading colon, auto-detect provider."""
    spec = spec.removeprefix(":")
    parsed = ModelSpec.try_parse(spec)
    if not parsed:
        provider = detect_provider(spec)
        if provider:
            spec = f"{provider}:{spec}"
    return spec


def set_default(spec: str) -> DefaultResult:
    """Normalize *spec* and persist as default. Does not affect the running session."""
    spec = _normalize_spec(spec)
    if save_default_model(spec):
        return DefaultResult(ok=True, display_name=spec)
    return DefaultResult(
        ok=False,
        error="Could not save default model. Check permissions for ~/.icarus/",
    )


def clear_default() -> DefaultResult:
    """Remove default model; future launches fall back to recent or auto-detect."""
    if clear_default_model():
        return DefaultResult(ok=True)
    return DefaultResult(
        ok=False,
        error="Could not clear default model. Check permissions for ~/.icarus/",
    )


def switch_model(
    spec: str,
    *,
    ctx: UserContext,
    mcp_tools: list[Any] | None = None,
    checkpointer: BaseCheckpointSaver,
    auto_approve: bool = False,
) -> SwitchResult:
    """Switch to a new model, preserving conversation history.

    Validates credentials, creates the model, updates global settings
    (with rollback on failure), and recompiles the agent graph.

    Args:
        spec: Model spec — ``"provider:model"`` or bare model name.
        ctx: UserContext for agent creation.
        mcp_tools: MCP tools to inject into the new agent.
        checkpointer: Checkpoint saver for session persistence.
        auto_approve: HITL setting for the new agent.

    Returns:
        SwitchResult with the new agent on success, or error details.
    """
    from icarus.runtime.builder import create_icarus_agent

    # Strip leading colon — treat ":claude-opus-4-6" as "claude-opus-4-6"
    spec = spec.removeprefix(":")

    parsed = ModelSpec.try_parse(spec)
    if parsed:
        provider: str | None = parsed.provider
        model_name = parsed.model
    else:
        model_name = spec
        provider = detect_provider(spec)

    # --- Credential check ---
    if provider and has_provider_credentials(provider) is False:
        env_var = get_credential_env_var(provider)
        if env_var:
            detail = f"{env_var} is not set or is empty"
        else:
            detail = (
                f"provider '{provider}' is not recognized. "
                "Add it to ~/.icarus/config.toml with an api_key_env field"
            )
        return SwitchResult(ok=False, error=f"Missing credentials: {detail}")

    # --- Already active check ---
    if model_name == settings.model_name and (
        not provider or provider == settings.model_provider
    ):
        display = f"{settings.model_provider}:{settings.model_name}"
        return SwitchResult(ok=False, display_name=display, already_active=True)

    # --- Create the model ---
    try:
        result = create_model(spec)
    except ModelConfigError as e:
        return SwitchResult(ok=False, error=str(e))
    except Exception as e:
        logger.exception("Failed to create model from spec %s", spec)
        return SwitchResult(ok=False, error=f"Failed to create model: {e}")

    # --- Update settings BEFORE agent creation ---
    # System prompt reads settings to describe the model to itself.
    # Save previous values for rollback if agent creation fails.
    prev_name = settings.model_name
    prev_provider = settings.model_provider
    prev_context_limit = settings.model_context_limit
    result.apply_to_settings()

    # --- Recompile agent graph ---
    try:
        agent_result = create_icarus_agent(
            ctx,
            model=result.model,
            tools=mcp_tools or None,
            auto_approve=auto_approve,
            checkpointer=checkpointer,
        )
    except Exception as e:
        # Rollback so the running agent isn't misrepresented
        settings.model_name = prev_name
        settings.model_provider = prev_provider
        settings.model_context_limit = prev_context_limit
        logger.exception("Failed to create agent for model switch")
        return SwitchResult(ok=False, error=f"Model switch failed: {e}")

    # --- Persist preference ---
    display = f"{settings.model_provider}:{settings.model_name}"
    saved = save_recent_model(display)

    return SwitchResult(
        ok=True,
        display_name=display,
        graph=agent_result.graph,
        backend=agent_result.backend,
        model=agent_result.model,
        preference_saved=saved,
    )
