"""Shared helper methods for Telegram command handlers."""

from __future__ import annotations

import json
import shlex
from typing import TYPE_CHECKING, Any, Optional

import httpx

from .....agents.opencode.client import OpenCodeProtocolError
from .....agents.opencode.supervisor import OpenCodeSupervisorError
from .....agents.registry import get_registered_agents, normalize_agent_capabilities
from .....core.coercion import coerce_int
from ....chat.agents import DEFAULT_CHAT_AGENT, normalize_chat_agent
from ...adapter import InlineButton, build_inline_keyboard, encode_cancel_callback
from ...helpers import format_public_error
from ...payload_utils import (
    extract_opencode_error_detail,
    extract_opencode_session_path,
)

if TYPE_CHECKING:
    pass


class TelegramCommandSupportMixin:
    """Shared helper methods for Telegram command handlers.

    This mixin provides common utilities for error formatting, argument parsing,
    and OpenCode session handling across Telegram command handler classes.
    All methods use `self` to access instance attributes.
    """

    def _coerce_int(self, value: Any) -> Optional[int]:
        """Safely coerce value to int, rejecting bool.

        Args:
            value: Value to coerce to int.

        Returns:
            Integer value if coercion succeeds and value is not bool, None otherwise.
        """
        return coerce_int(value)

    def _agent_descriptor(self, agent: object) -> Any:
        normalized = normalize_chat_agent(
            agent,
            default=DEFAULT_CHAT_AGENT,
            context=self,
        )
        if normalized is None:
            return None
        try:
            descriptors = get_registered_agents(self)
        except TypeError as exc:
            if "positional argument" not in str(exc):
                raise
            descriptors = get_registered_agents()
        return descriptors.get(normalized)

    def _agent_display_name(self, agent: object) -> str:
        descriptor = self._agent_descriptor(agent)
        if descriptor is not None:
            return descriptor.name
        normalized = normalize_chat_agent(agent, context=self)
        if isinstance(normalized, str) and normalized:
            return normalized
        if isinstance(agent, str) and agent.strip():
            return agent.strip()
        return "This agent"

    def _agent_supports_capability(self, agent: object, capability: str) -> bool:
        descriptor = self._agent_descriptor(agent)
        if descriptor is None:
            return False
        normalized = normalize_agent_capabilities([capability])
        if not normalized:
            return False
        return next(iter(normalized)) in descriptor.capabilities

    def _agents_supporting_capability(self, capability: str) -> list[str]:
        normalized = normalize_agent_capabilities([capability])
        if not normalized:
            return []
        resolved = next(iter(normalized))
        try:
            descriptors = get_registered_agents(self)
        except TypeError as exc:
            if "positional argument" not in str(exc):
                raise
            descriptors = get_registered_agents()
        return sorted(
            descriptor.id
            for descriptor in descriptors.values()
            if resolved in descriptor.capabilities
        )

    def _format_httpx_exception(self, exc: Exception) -> Optional[str]:
        """Format httpx exceptions for user-friendly error messages.

        Args:
            exc: Exception to format.

        Returns:
            Formatted error message string or None if exception is not an httpx error.
        """
        if isinstance(exc, httpx.HTTPStatusError):
            try:
                payload = exc.response.json()
            except Exception:
                payload = None
            if isinstance(payload, dict):
                detail = (
                    payload.get("detail")
                    or payload.get("message")
                    or payload.get("error")
                )
                if isinstance(detail, str) and detail:
                    return format_public_error(detail)
            response_text = exc.response.text.strip()
            if response_text:
                return format_public_error(response_text)
            return f"Request failed (HTTP {exc.response.status_code})."
        if isinstance(exc, httpx.RequestError):
            detail = str(exc).strip()
            if detail:
                return format_public_error(detail)
            return "Request failed."
        return None

    def _format_opencode_exception(self, exc: Exception) -> Optional[str]:
        """Format OpenCode exceptions for user-friendly error messages.

        Args:
            exc: Exception to format.

        Returns:
            Formatted error message string or None if exception is not recognized.
        """
        if isinstance(exc, OpenCodeSupervisorError):
            detail = str(exc).strip()
            if detail:
                return f"OpenCode backend unavailable ({format_public_error(detail)})."
            return "OpenCode backend unavailable."
        if isinstance(exc, OpenCodeProtocolError):
            detail = str(exc).strip()
            if detail:
                return f"OpenCode protocol error: {format_public_error(detail)}"
            return "OpenCode protocol error."
        if isinstance(exc, json.JSONDecodeError):
            return "OpenCode returned invalid JSON."
        if isinstance(exc, httpx.HTTPStatusError):
            detail = None
            try:
                detail = self._extract_opencode_error_detail(exc.response.json())
            except Exception:
                detail = None
            if detail:
                return f"OpenCode error: {format_public_error(detail)}"
            response_text = exc.response.text.strip()
            if response_text:
                return f"OpenCode error: {format_public_error(response_text)}"
            return f"OpenCode request failed (HTTP {exc.response.status_code})."
        if isinstance(exc, httpx.RequestError):
            detail = str(exc).strip()
            if detail:
                return f"OpenCode request failed: {format_public_error(detail)}"
            return "OpenCode request failed."
        return None

    def _extract_opencode_error_detail(self, payload: Any) -> Optional[str]:
        """Extract error detail from OpenCode response payload.

        Args:
            payload: Response payload to extract error detail from.

        Returns:
            Error detail string if found, None otherwise.
        """
        return extract_opencode_error_detail(payload)

    def _extract_opencode_session_path(self, payload: Any) -> Optional[str]:
        """Extract session path from OpenCode payload.

        Args:
            payload: Payload to extract session path from.

        Returns:
            Session path string if found, None otherwise.
        """
        return extract_opencode_session_path(payload)

    def _is_missing_opencode_session_error(self, exc: Exception) -> bool:
        """Return true when an OpenCode error indicates a missing session."""

        markers = (
            "session not found",
            "no session found",
            "unknown session",
            "session does not exist",
            "session no longer exists",
            "missing session",
        )
        current: Optional[BaseException] = exc
        seen: set[int] = set()
        while current is not None and id(current) not in seen:
            seen.add(id(current))
            if isinstance(current, httpx.HTTPStatusError):
                if current.response.status_code == 404:
                    return True
                detail = None
                try:
                    detail = self._extract_opencode_error_detail(
                        current.response.json()
                    )
                except Exception:
                    detail = None
                candidates = [detail, current.response.text, str(current)]
            else:
                candidates = [str(current)]
            for candidate in candidates:
                if not isinstance(candidate, str):
                    continue
                lowered = candidate.lower()
                if any(marker in lowered for marker in markers):
                    return True
                if "session" in lowered and "not found" in lowered:
                    return True
            current = current.__cause__ or current.__context__
        return False

    def _opencode_session_stall_timeout_seconds(self) -> Optional[float]:
        """Return configured OpenCode stream stall timeout when available."""

        opencode_cfg = getattr(getattr(self, "_config", None), "opencode", None)
        value = getattr(opencode_cfg, "session_stall_timeout_seconds", None)
        if value is None:
            return None
        try:
            parsed = float(value)
        except (TypeError, ValueError):
            return None
        if parsed <= 0:
            return None
        return parsed

    def _interrupt_keyboard(self) -> dict[str, Any]:
        """Build interrupt button keyboard.

        Returns:
            Inline keyboard with a cancel/interrupt button.
        """
        return build_inline_keyboard(
            [[InlineButton("Cancel", encode_cancel_callback("interrupt"))]]
        )

    async def _clear_interrupt_status_message(
        self,
        *,
        chat_id: int,
        runtime: Any,
        turn_id: Optional[str],
        fallback_text: Optional[str] = None,
        outcome_visible: bool = True,
    ) -> None:
        """Clear an interrupt status message once the turn outcome is already visible.

        Prefer deleting the transient "Stopping current turn..." message so Telegram
        does not restate the same terminal outcome in a second message. When the turn
        outcome was not delivered, preserve a terminal signal by editing in place.
        """

        if runtime.interrupt_turn_id != turn_id:
            return
        message_id = runtime.interrupt_message_id
        if message_id is None:
            runtime.interrupt_turn_id = None
            return
        cleared = False
        if outcome_visible:
            cleared = await self._delete_message(chat_id, message_id)
        if not cleared and fallback_text:
            await self._edit_message_text(chat_id, message_id, fallback_text)
        runtime.interrupt_message_id = None
        runtime.interrupt_turn_id = None

    def _parse_command_args(self, args: str) -> list[str]:
        """Parse command arguments with shlex.

        Args:
            args: Command argument string.

        Returns:
            List of parsed argument tokens.
        """
        if not args:
            return []
        try:
            return [part for part in shlex.split(args) if part]
        except ValueError:
            return [part for part in args.split() if part]
