"""Interactive model selector wizard for Telegram inline keyboards."""

from __future__ import annotations

import asyncio
import logging
from dataclasses import dataclass
from typing import TYPE_CHECKING

from sygen_bot.cli.auth import AuthStatus, check_all_auth
from sygen_bot.config import CLAUDE_MODELS_ORDERED, get_gemini_models
from sygen_bot.i18n import t

from sygen_bot.orchestrator.selectors.models import Button, ButtonGrid, SelectorResponse

if TYPE_CHECKING:
    from sygen_bot.cli.codex_cache import CodexModelCache
    from sygen_bot.orchestrator.core import Orchestrator
    from sygen_bot.session import SessionData
    from sygen_bot.session.key import SessionKey

logger = logging.getLogger(__name__)

MS_PREFIX = "ms:"

_EFFORT_LABELS: dict[str, str] = {
    "low": "Low",
    "medium": "Medium",
    "high": "High",
    "xhigh": "XHigh",
}


@dataclass(frozen=True)
class _SwitchSummaryContext:
    old_model: str
    new_model: str
    old_provider: str
    new_provider: str
    provider_changed: bool
    reasoning_effort: str | None
    effort_only: bool
    resume_session_id: str
    resume_message_count: int


def _resume_state_for_provider(session: SessionData | None, provider: str) -> tuple[str, int]:
    """Return (session_id, message_count) for provider if resumable history exists."""
    if session is None:
        return "", 0
    provider_data = session.provider_sessions.get(provider)
    if provider_data is None or provider_data.message_count <= 0:
        return "", 0
    return provider_data.session_id, provider_data.message_count


def _format_resume_hint(session_id: str, message_count: int, model_id: str) -> str:
    """Build post-switch resume hint text."""
    message_label = t("model.resume_msg_one") if message_count == 1 else t("model.resume_msg_other")
    sid_display = session_id or "pending"
    return "\n" + t(
        "model.resume_hint",
        sid=sid_display,
        count=message_count,
        label=message_label,
        model=model_id,
    )


def _build_switch_summary(ctx: _SwitchSummaryContext) -> str:
    """Build user-facing model switch summary text."""
    parts: list[str] = [t("model.switched")]
    if ctx.old_model == ctx.new_model:
        parts.append(t("model.model_line", model=ctx.new_model))
    else:
        parts.append(t("model.model_change", old=ctx.old_model, new=ctx.new_model))
    if ctx.provider_changed:
        parts.append(t("model.provider_change", old=ctx.old_provider, new=ctx.new_provider))
    if ctx.reasoning_effort:
        parts.append(t("model.reasoning_line", effort=ctx.reasoning_effort))
    if ctx.old_model != ctx.new_model and ctx.resume_message_count > 0:
        parts.append(
            _format_resume_hint(
                ctx.resume_session_id,
                ctx.resume_message_count,
                ctx.new_model,
            )
        )
    if ctx.effort_only:
        parts.append("\n" + t("model.effort_updated"))
    return "\n".join(parts)


# Curated set of Gemini models shown in the /model selector.
# Only actively supported models belong here; the full discovered set is
# still accepted for direct /model <id> commands.
_SELECTOR_GEMINI_MODELS: tuple[str, ...] = (
    "auto-gemini-3.1",
    "gemini-3.1-pro-preview",
    "gemini-3-flash-preview",
    "gemini-3.1-flash-lite-preview",
)


def _gemini_models_for_selector() -> list[str]:
    """Return curated Gemini models for the /model keyboard.

    Shows only the curated list, filtering out entries that weren't
    discovered (or cached) so stale models don't appear as buttons.
    """
    discovered = get_gemini_models()
    if not discovered:
        # No cache at all — return the curated list as-is (fallback).
        return list(_SELECTOR_GEMINI_MODELS)
    return [m for m in _SELECTOR_GEMINI_MODELS if m in discovered]


_BUTTON_LABELS: dict[str, str] = {
    "auto-gemini-3.1": "Auto 3.1",
    "gemini-3.1-pro-preview": "Pro 3.1",
    "gemini-3-flash-preview": "Flash 3",
    "gemini-3.1-flash-lite-preview": "Lite 3.1",
}


def _button_label(model_id: str) -> str:
    """Compact button label while preserving identity in callback data."""
    if model_id in _BUTTON_LABELS:
        return _BUTTON_LABELS[model_id]
    if model_id.startswith("auto-gemini-"):
        return "auto " + model_id.removeprefix("auto-gemini-")
    return model_id.removeprefix("gemini-")


def _chunk_buttons(
    model_ids: list[str],
    *,
    columns: int = 3,
) -> list[list[Button]]:
    rows: list[list[Button]] = []
    for index in range(0, len(model_ids), columns):
        chunk = model_ids[index : index + columns]
        rows.append(
            [
                Button(
                    text=_button_label(model_id),
                    callback_data=f"ms:m:{model_id}",
                )
                for model_id in chunk
            ]
        )
    return rows


def is_model_selector_callback(data: str) -> bool:
    """Return True if *data* belongs to the model selector wizard."""
    return data.startswith(MS_PREFIX)


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------


async def model_selector_start(
    orch: Orchestrator,
    key: SessionKey,
) -> SelectorResponse:
    """Build the initial ``/model`` response with provider buttons.

    Returns a ``SelectorResponse``. Buttons are ``None`` when no providers
    are authenticated.
    """
    auth = await asyncio.to_thread(check_all_auth)
    authed = [name for name, res in auth.items() if res.status == AuthStatus.AUTHENTICATED]

    header = await _status_line(orch, key)

    if not authed:
        return SelectorResponse(
            text=f"{header}\n\n{t('model.no_auth')}",
        )

    if len(authed) == 1:
        provider = authed[0]
        codex_cache = (
            orch._observers.codex_cache_obs.get_cache() if orch._observers.codex_cache_obs else None
        )
        return await _build_model_step(provider, header, codex_cache)

    buttons: list[Button] = []
    if "claude" in authed:
        buttons.append(Button(text="CLAUDE", callback_data="ms:p:claude"))
    if "codex" in authed:
        buttons.append(Button(text="CODEX", callback_data="ms:p:codex"))
    if "gemini" in authed:
        buttons.append(Button(text="GEMINI", callback_data="ms:p:gemini"))

    keyboard = ButtonGrid(rows=[buttons])
    return SelectorResponse(text=f"{header}\n\n{t('model.pick_provider')}", buttons=keyboard)


async def handle_model_callback(
    orch: Orchestrator,
    key: SessionKey,
    data: str,
) -> SelectorResponse:
    """Route an ``ms:*`` callback to the correct wizard step.

    Returns a ``SelectorResponse`` for editing the message in-place.
    """
    logger.debug("Model selector step=%s", data[:40])
    parts = data[len(MS_PREFIX) :].split(":", 2)
    action = parts[0] if parts else ""
    payload = parts[1] if len(parts) > 1 else ""
    extra = parts[2] if len(parts) > 2 else ""

    codex_cache = (
        orch._observers.codex_cache_obs.get_cache() if orch._observers.codex_cache_obs else None
    )

    if action == "p":
        return await _build_model_step(payload, await _status_line(orch, key), codex_cache)

    if action == "m":
        return await _handle_model_selected(orch, key, payload, codex_cache)

    if action == "r":
        return await _handle_reasoning_selected(orch, key, effort=payload, model_id=extra)

    if action == "b":
        if payload == "root":
            return await model_selector_start(orch, key)
        return await _build_model_step(payload, await _status_line(orch, key), codex_cache)

    logger.warning("Unknown model selector callback: %s", data)
    return SelectorResponse(text=t("model.unknown_action"))


async def switch_model(
    orch: Orchestrator,
    key: SessionKey,
    model_id: str,
    *,
    reasoning_effort: str | None = None,
) -> str:
    """Execute model switch: kill processes, preserve sessions, persist config.

    Shared by ``/model <name>`` text command and the wizard callbacks.
    """
    is_topic = key.topic_id is not None
    active_session = await orch._sessions.get_active(key)

    old = active_session.model if is_topic and active_session else orch._config.model
    same_model = old == model_id
    # For non-topic chats the in-memory config may have drifted from the
    # active session (e.g. after /new or daily reset).  Treat the switch as
    # required when the session model disagrees with the requested one.
    if not is_topic and active_session and active_session.model != model_id:
        same_model = False
    effort_only = same_model and reasoning_effort is not None

    if same_model and reasoning_effort is None:
        return t("model.already_running", model=model_id)

    old_provider = orch.models.provider_for(old)
    new_provider = orch.models.provider_for(model_id)
    provider_changed = old_provider != new_provider
    resume_session_id, resume_message_count = _resume_state_for_provider(
        active_session,
        new_provider,
    )

    if not same_model:
        await orch._process_registry.kill_all(key.chat_id)
        if active_session is not None:
            await orch._sessions.sync_session_target(
                active_session,
                provider=new_provider,
                model=model_id,
            )

    if not is_topic:
        # Update in-memory config for the current runtime (new sessions,
        # CLI service fallback).  Do NOT persist to config.json so that
        # /new always resets to the startup default model.
        orch._config.model = model_id
        orch._cli_service.update_default_model(model_id)
        if provider_changed:
            orch._config.provider = new_provider

        if reasoning_effort is not None:
            orch._config.reasoning_effort = reasoning_effort
            orch._cli_service.update_reasoning_effort(reasoning_effort)

    is_busy = orch.is_chat_busy(key.chat_id, key.topic_id)
    logger.info("Model switch model=%s provider=%s", model_id, orch._config.provider)

    summary = _build_switch_summary(
        _SwitchSummaryContext(
            old_model=old,
            new_model=model_id,
            old_provider=old_provider,
            new_provider=new_provider,
            provider_changed=provider_changed,
            reasoning_effort=reasoning_effort,
            effort_only=effort_only,
            resume_session_id=resume_session_id,
            resume_message_count=resume_message_count,
        )
    )
    if is_busy:
        summary += f"\n\n_{t('model.switched_while_busy')}_"
    return summary


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------


async def _status_line(orch: Orchestrator, key: SessionKey) -> str:
    """Current model + reasoning effort as a short header."""
    session = await orch._sessions.get_active(key)
    if session:
        model = session.model
        provider = session.provider
    else:
        model, provider = orch.resolve_runtime_target(orch._config.model)

    effort = orch._config.reasoning_effort

    if provider == "codex":
        current = (
            f"{t('model.header')}\n{t('model.current_with_effort', model=model, effort=effort)}"
        )
    else:
        current = f"{t('model.header')}\n{t('model.current', model=model)}"

    if key.topic_id is None:
        configured = orch._config.model
        if model != configured:
            current += f"\n{t('model.configured_default', configured=configured)}"

    return current


async def _build_model_step(
    provider: str,
    header: str,
    codex_cache: CodexModelCache | None = None,
) -> SelectorResponse:
    """Build the model selection keyboard for a provider."""
    if provider == "claude":
        buttons = [Button(text=m.upper(), callback_data=f"ms:m:{m}") for m in CLAUDE_MODELS_ORDERED]
        keyboard = ButtonGrid(
            rows=[
                buttons,
                [Button(text=t("model.btn_back"), callback_data="ms:b:root")],
            ]
        )
        return SelectorResponse(text=f"{header}\n\n{t('model.select_claude')}", buttons=keyboard)

    if provider == "gemini":
        gemini_models = _gemini_models_for_selector()
        if not gemini_models:
            keyboard = ButtonGrid(
                rows=[
                    [Button(text=t("model.btn_back"), callback_data="ms:b:root")],
                ]
            )
            return SelectorResponse(
                text=f"{header}\n\n{t('model.no_gemini')}",
                buttons=keyboard,
            )

        gemini_rows = _chunk_buttons(gemini_models)
        gemini_rows.append([Button(text=t("model.btn_back"), callback_data="ms:b:root")])
        keyboard = ButtonGrid(rows=gemini_rows)
        return SelectorResponse(text=f"{header}\n\n{t('model.select_gemini')}", buttons=keyboard)

    # Use cache instead of live discovery
    codex_models = codex_cache.models if codex_cache else []
    if not codex_models:
        keyboard = ButtonGrid(
            rows=[
                [Button(text=t("model.btn_back"), callback_data="ms:b:root")],
            ]
        )
        return SelectorResponse(text=f"{header}\n\n{t('model.no_codex')}", buttons=keyboard)

    rows: list[list[Button]] = [
        [Button(text=m.display_name, callback_data=f"ms:m:{m.id}")] for m in codex_models
    ]
    rows.append([Button(text=t("model.btn_back"), callback_data="ms:b:root")])

    keyboard = ButtonGrid(rows=rows)
    return SelectorResponse(text=f"{header}\n\n{t('model.select_codex')}", buttons=keyboard)


async def _handle_model_selected(
    orch: Orchestrator,
    key: SessionKey,
    model_id: str,
    codex_cache: CodexModelCache | None = None,
) -> SelectorResponse:
    """Handle a model button press. Claude/Gemini: switch immediately. Codex: show reasoning."""
    provider = orch.models.provider_for(model_id)

    if provider in ("claude", "gemini"):
        result = await switch_model(orch, key, model_id)
        return SelectorResponse(text=result)

    # Use cache instead of live discovery
    codex_info = codex_cache.get_model(model_id) if codex_cache else None
    efforts = codex_info.supported_efforts if codex_info else ("low", "medium", "high", "xhigh")

    buttons = [
        Button(
            text=_EFFORT_LABELS.get(e, e),
            callback_data=f"ms:r:{e}:{model_id}",
        )
        for e in efforts
    ]
    keyboard = ButtonGrid(
        rows=[
            buttons,
            [Button(text=t("model.btn_back"), callback_data="ms:b:codex")],
        ]
    )

    header = await _status_line(orch, key)
    return SelectorResponse(
        text=f"{header}\n\n{t('model.thinking_level', model=model_id)}", buttons=keyboard
    )


async def _handle_reasoning_selected(
    orch: Orchestrator,
    key: SessionKey,
    *,
    effort: str,
    model_id: str,
) -> SelectorResponse:
    """Handle a reasoning effort button press. Final step: switch model + effort."""
    result = await switch_model(orch, key, model_id, reasoning_effort=effort)
    return SelectorResponse(text=result)
