"""
Telegram channel implementation.

Uses httpx for direct Bot API calls (no framework dependency).
Long-polling via getUpdates. Inline keyboard for YES_NO / MULTIPLE_CHOICE.

Callback data format:
  ans:{prompt_id}:{session_id}:{nonce}:{value}

  Components:
    prompt_id  — 24-char hex (secrets.token_hex(12))
    session_id — UUID4
    nonce      — 16-char hex (secrets.token_hex(8))
    value      — y | n | 1 | 2 | ... (URL-safe; no colons)

  Max total length: ~120 bytes (Telegram limit: 64 bytes of callback_data)
  If the compound key exceeds 64 bytes, the nonce is stored server-side
  and the callback_data uses a short reference key.

Rate limiting:
  Telegram allows ~30 messages/second per bot. Aegis sends at most
  1 prompt per session at a time, so rate limiting is not a concern
  for normal usage. The channel implements exponential backoff on 429.

Allowlist:
  Config key: channels.telegram.allowed_user_ids (list of int)
  Only users in this list can trigger injections.
"""

from __future__ import annotations

import asyncio
import logging
import secrets
from collections.abc import AsyncIterator
from datetime import UTC
from typing import Any

from atlasbridge.channels.base import BaseChannel
from atlasbridge.core.prompt.models import Confidence, PromptEvent, PromptType, Reply

logger = logging.getLogger(__name__)

_BASE_URL = "https://api.telegram.org/bot{token}/{method}"
_POLL_TIMEOUT = 30  # Long-poll timeout (seconds)
_RETRY_BASE_S = 1.0
_RETRY_MAX_S = 60.0


class TelegramChannel(BaseChannel):
    """
    Telegram Bot API channel.

    Requires:
      bot_token        — Telegram Bot API token (from @BotFather)
      allowed_user_ids — list of Telegram user IDs permitted to reply
    """

    channel_name = "telegram"
    display_name = "Telegram"

    def __init__(self, bot_token: str, allowed_user_ids: list[int]) -> None:
        self._token = bot_token
        self._allowed = set(allowed_user_ids)
        self._reply_queue: asyncio.Queue[Reply] = asyncio.Queue()
        self._offset = 0  # getUpdates offset
        self._running = False
        self._client = None  # httpx.AsyncClient — created in start()

    async def start(self) -> None:
        try:
            import httpx  # type: ignore[import]
        except ImportError as exc:
            raise RuntimeError(
                "httpx is required for the Telegram channel. Install with: pip install httpx"
            ) from exc

        self._client = httpx.AsyncClient(timeout=_POLL_TIMEOUT + 5)
        self._running = True
        asyncio.create_task(self._poll_loop(), name="telegram_poll")
        logger.info("Telegram channel started")

    async def close(self) -> None:
        self._running = False
        if self._client:
            await self._client.aclose()
        logger.info("Telegram channel closed")

    async def send_prompt(self, event: PromptEvent) -> str:
        """Send a prompt message with an inline keyboard."""
        text = self._format_prompt(event)
        keyboard = self._build_keyboard(event)
        payload: dict[str, Any] = {"text": text, "parse_mode": "HTML"}
        if keyboard:
            payload["reply_markup"] = {"inline_keyboard": keyboard}

        responses = []
        for uid in self._allowed:
            payload["chat_id"] = uid
            resp = await self._api("sendMessage", payload)
            if resp:
                responses.append(str(resp.get("message_id", "")))

        return responses[0] if responses else ""

    async def notify(self, message: str, session_id: str = "") -> None:
        for uid in self._allowed:
            await self._api("sendMessage", {"chat_id": uid, "text": message})

    async def edit_prompt_message(
        self,
        message_id: str,
        new_text: str,
        session_id: str = "",
    ) -> None:
        for uid in self._allowed:
            await self._api(
                "editMessageText",
                {
                    "chat_id": uid,
                    "message_id": int(message_id),
                    "text": new_text,
                },
            )

    async def receive_replies(self) -> AsyncIterator[Reply]:  # type: ignore[override]
        while self._running:
            try:
                reply = await asyncio.wait_for(self._reply_queue.get(), timeout=1.0)
                yield reply
            except TimeoutError:
                continue

    def is_allowed(self, identity: str) -> bool:
        # identity = "telegram:123456789"
        try:
            _, uid_str = identity.split(":", 1)
            return int(uid_str) in self._allowed
        except (ValueError, AttributeError):
            return False

    def healthcheck(self) -> dict[str, Any]:
        return {
            "status": "ok" if self._running else "stopped",
            "channel": self.channel_name,
            "allowed_users": len(self._allowed),
        }

    # ------------------------------------------------------------------
    # Private helpers
    # ------------------------------------------------------------------

    async def _poll_loop(self) -> None:
        """Long-polling loop — runs for the lifetime of the daemon."""
        backoff = _RETRY_BASE_S
        while self._running:
            try:
                updates = await self._api(
                    "getUpdates",
                    {
                        "offset": self._offset,
                        "timeout": _POLL_TIMEOUT,
                        "allowed_updates": ["message", "callback_query"],
                    },
                )
                if updates:
                    for update in updates:
                        self._offset = update["update_id"] + 1
                        await self._handle_update(update)
                backoff = _RETRY_BASE_S
            except Exception:  # noqa: BLE001
                logger.warning("Telegram polling error; backoff=%.1fs", backoff)
                await asyncio.sleep(backoff)
                backoff = min(backoff * 2, _RETRY_MAX_S)

    async def _handle_update(self, update: dict[str, Any]) -> None:
        if "callback_query" in update:
            await self._handle_callback(update["callback_query"])
        elif "message" in update:
            await self._handle_message(update["message"])

    async def _handle_callback(self, cb: dict[str, Any]) -> None:
        """Handle inline keyboard button tap."""
        user_id = cb.get("from", {}).get("id")
        data = cb.get("data", "")

        if not self.is_allowed(f"telegram:{user_id}"):
            logger.warning("Rejected callback from unknown user %s", user_id)
            return

        # Parse: ans:{prompt_id}:{session_id}:{nonce}:{value}
        try:
            parts = data.split(":", 4)
            if parts[0] != "ans" or len(parts) != 5:
                return
            _, prompt_id, session_id, nonce, value = parts
        except ValueError:
            return

        reply = Reply(
            prompt_id=prompt_id,
            session_id=session_id,
            value=value,
            nonce=nonce,
            channel_identity=f"telegram:{user_id}",
            timestamp=_utcnow(),
        )
        await self._reply_queue.put(reply)

        # Acknowledge the callback to remove the spinner on the button
        await self._api("answerCallbackQuery", {"callback_query_id": cb["id"]})

    async def _handle_message(self, msg: dict[str, Any]) -> None:
        """Handle free-text reply messages."""
        user_id = msg.get("from", {}).get("id")
        text = msg.get("text", "").strip()

        if not text or text.startswith("/"):
            return  # Commands handled elsewhere
        if not self.is_allowed(f"telegram:{user_id}"):
            return

        # Free-text reply: nonce is derived from message_id for idempotency
        nonce = secrets.token_hex(8)
        reply = Reply(
            prompt_id="",  # Router resolves the active prompt for this session
            session_id="",  # Router resolves by active session
            value=text,
            nonce=nonce,
            channel_identity=f"telegram:{user_id}",
            timestamp=_utcnow(),
            newline_policy="append",
        )
        await self._reply_queue.put(reply)

    async def _api(self, method: str, payload: dict[str, Any]) -> Any:
        """Make a Telegram Bot API call. Returns the result dict or None on error."""
        if self._client is None:
            return None
        url = _BASE_URL.format(token=self._token, method=method)
        try:
            resp = await self._client.post(url, json=payload)
            data = resp.json()
            if data.get("ok"):
                return data.get("result")
            logger.warning("Telegram API error: %s", data)
        except Exception as exc:  # noqa: BLE001
            logger.warning("Telegram API request failed: %s", exc)
        return None

    @staticmethod
    def _format_prompt(event: PromptEvent) -> str:
        type_labels = {
            PromptType.TYPE_YES_NO: "Yes / No",
            PromptType.TYPE_CONFIRM_ENTER: "Press Enter",
            PromptType.TYPE_MULTIPLE_CHOICE: "Multiple Choice",
            PromptType.TYPE_FREE_TEXT: "Free Text",
        }
        confidence_labels = {
            Confidence.HIGH: "high",
            Confidence.MED: "medium",
            Confidence.LOW: "low (ambiguous)",
        }
        label = type_labels.get(event.prompt_type, event.prompt_type)
        conf = confidence_labels.get(event.confidence, event.confidence)
        return (
            f"<b>Aegis</b> — session <code>{event.session_id[:8]}</code> "
            f"is waiting for input\n\n"
            f"<pre>{event.excerpt}</pre>\n\n"
            f"Type: {label} | Confidence: {conf}"
        )

    @staticmethod
    def _build_keyboard(event: PromptEvent) -> list[list[dict[str, str]]]:
        """Build Telegram inline keyboard buttons."""
        base = f"ans:{event.prompt_id}:{event.session_id}:{event.idempotency_key}"

        if event.prompt_type == PromptType.TYPE_YES_NO:
            return [
                [
                    {"text": "Yes", "callback_data": f"{base}:y"},
                    {"text": "No", "callback_data": f"{base}:n"},
                ]
            ]
        if event.prompt_type == PromptType.TYPE_CONFIRM_ENTER:
            return [
                [
                    {"text": "Send Enter", "callback_data": f"{base}:enter"},
                    {"text": "Cancel", "callback_data": f"{base}:cancel"},
                ]
            ]
        if event.prompt_type == PromptType.TYPE_MULTIPLE_CHOICE:
            return [
                [
                    {"text": str(i + 1), "callback_data": f"{base}:{i + 1}"}
                    for i in range(len(event.choices))
                ]
            ]
        return []  # FREE_TEXT: no buttons; user replies via message


def _utcnow() -> str:
    from datetime import datetime

    return datetime.now(UTC).isoformat()
