"""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 .....core.coercion import coerce_int
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 SharedHelpers:
    """Shared helper methods for Telegram command handlers.

    This class is designed to be used as a mixin in 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 _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"))]]
        )

    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]
