from __future__ import annotations

import asyncio
import os
import re
import threading
from pathlib import Path
from typing import Any, Mapping, Optional

from telegram import Bot, BotCommand

from armorer.messaging.gateway import MessagingGateway
from armorer.messaging.models import MessagingEvent, UserMapping


_MDV2_SPECIAL = re.compile(r"([_*\[\]()~`>#+\-=|{}.!\\\\])")


def escape_markdown_v2(text: str) -> str:
    """Escape Telegram MarkdownV2 special characters."""
    return _MDV2_SPECIAL.sub(r"\\\1", str(text or ""))


class TelegramGateway(MessagingGateway):
    provider = "telegram"

    def __init__(self, bot_token: Optional[str] = None, api_base: str = "https://api.telegram.org", timeout: int = 15):
        self.bot_token = bot_token or os.getenv("ARMORER_TELEGRAM_BOT_TOKEN") or os.getenv("TELEGRAM_BOT_TOKEN")
        if not self.bot_token:
            raise ValueError(
                "Telegram bot token is required. Set ARMORER_TELEGRAM_BOT_TOKEN, TELEGRAM_BOT_TOKEN, or pass bot_token."
            )
        self.api_base = api_base.rstrip("/")
        self.timeout = timeout
        self._bot = Bot(token=self.bot_token, base_url=f"{self.api_base}/bot")
        self._loop = asyncio.new_event_loop()
        self._loop_ready = threading.Event()
        self._loop_thread = threading.Thread(target=self._loop_worker, name="armorer-telegram-loop", daemon=True)
        self._loop_thread.start()
        self._loop_ready.wait(timeout=2.0)

    def _loop_worker(self) -> None:
        asyncio.set_event_loop(self._loop)
        self._loop_ready.set()
        self._loop.run_forever()

    def _run(self, awaitable: Any) -> Any:
        future = asyncio.run_coroutine_threadsafe(awaitable, self._loop)
        return future.result()

    def close(self) -> None:
        try:
            self._run(self._bot.shutdown())
        except Exception:
            pass
        try:
            self._loop.call_soon_threadsafe(self._loop.stop)
        except Exception:
            pass
        if self._loop_thread.is_alive():
            self._loop_thread.join(timeout=2.0)
        if not self._loop.is_closed():
            try:
                self._loop.close()
            except Exception:
                pass

    def __del__(self) -> None:
        try:
            self.close()
        except Exception:
            pass

    def _to_result_dict(self, value: Any) -> Any:
        if hasattr(value, "to_dict"):
            return value.to_dict()
        if isinstance(value, (list, tuple)):
            out = []
            for item in value:
                if hasattr(item, "to_dict"):
                    out.append(item.to_dict())
                else:
                    out.append(item)
            return out
        return value

    def _api_post(self, method: str, payload: Mapping[str, Any], timeout: Optional[int] = None) -> dict[str, Any]:
        p = dict(payload or {})
        if timeout is not None:
            p.setdefault("read_timeout", int(timeout))
        if method == "sendMessage":
            result = self._run(self._bot.send_message(**p))
        elif method == "getMe":
            result = self._run(self._bot.get_me(**p))
        elif method == "getUpdates":
            result = self._run(self._bot.get_updates(**p))
        elif method == "sendChatAction":
            result = self._run(self._bot.send_chat_action(**p))
        elif method == "getChat":
            result = self._run(self._bot.get_chat(**p))
        elif method == "answerCallbackQuery":
            result = self._run(self._bot.answer_callback_query(**p))
        elif method == "setMyCommands":
            result = self._run(self._bot.set_my_commands(**p))
        elif method == "getMyCommands":
            result = self._run(self._bot.get_my_commands(**p))
        else:
            raise RuntimeError(f"Unsupported Telegram API method: {method}")
        return {"ok": True, "result": self._to_result_dict(result)}

    def _api_post_multipart(
        self,
        method: str,
        fields: Mapping[str, Any],
        file_field: str,
        file_path: Path,
        content_type: str = "application/octet-stream",
        timeout: Optional[int] = None,
    ) -> dict[str, Any]:
        if method != "sendDocument":
            raise RuntimeError(f"Unsupported Telegram multipart method: {method}")
        payload = dict(fields or {})
        if timeout is not None:
            payload.setdefault("read_timeout", int(timeout))
        with file_path.open("rb") as f:
            payload[file_field] = f
            result = self._run(self._bot.send_document(**payload))
        return {"ok": True, "result": self._to_result_dict(result)}

    def send_message(
        self,
        channel_id: str,
        text: str,
        reply_to: Optional[str] = None,
        reply_markup: Optional[Mapping[str, Any]] = None,
        parse_mode: Optional[str] = None,
    ) -> dict[str, Any]:
        payload: dict[str, Any] = {"chat_id": channel_id, "text": text}
        if reply_to:
            payload["reply_to_message_id"] = int(reply_to)
        if reply_markup:
            payload["reply_markup"] = dict(reply_markup)
        if parse_mode:
            payload["parse_mode"] = str(parse_mode)
        data = self._api_post("sendMessage", payload)
        result = data.get("result", {}) if isinstance(data, dict) else {}
        return {
            "ok": bool(data.get("ok")),
            "provider": self.provider,
            "message_id": str(result.get("message_id")) if isinstance(result, dict) and result.get("message_id") is not None else None,
            "raw": data,
        }

    def get_me(self) -> dict[str, Any]:
        data = self._api_post("getMe", {})
        me = data.get("result", {}) if isinstance(data, dict) else {}
        if hasattr(me, "to_dict"):
            data = me.to_dict()
            return data if isinstance(data, dict) else {}
        return me if isinstance(me, dict) else {}

    def send_document(
        self,
        channel_id: str,
        document_path: str,
        caption: Optional[str] = None,
        parse_mode: Optional[str] = None,
    ) -> dict[str, Any]:
        p = Path(document_path)
        if not p.exists():
            raise FileNotFoundError(f"Document not found: {document_path}")
        fields: dict[str, Any] = {"chat_id": channel_id}
        if caption:
            fields["caption"] = caption
        if parse_mode:
            fields["parse_mode"] = parse_mode
        data = self._api_post_multipart(
            "sendDocument",
            fields,
            file_field="document",
            file_path=p,
        )
        result = data.get("result", {}) if isinstance(data, dict) else {}
        return {
            "ok": bool(data.get("ok")),
            "provider": self.provider,
            "message_id": str(result.get("message_id")) if isinstance(result, dict) and result.get("message_id") is not None else None,
            "raw": data,
        }

    def get_updates(
        self,
        offset: Optional[int] = None,
        timeout: int = 30,
        allowed_updates: Optional[list[str]] = None,
    ) -> list[dict[str, Any]]:
        payload: dict[str, Any] = {"timeout": timeout}
        if offset is not None:
            payload["offset"] = int(offset)
        if allowed_updates:
            payload["allowed_updates"] = allowed_updates
        request_timeout = max(self.timeout, int(timeout) + 5)
        data = self._api_post("getUpdates", payload, timeout=request_timeout)
        result = data.get("result", []) if isinstance(data, dict) else []
        return result if isinstance(result, list) else []

    def send_chat_action(self, channel_id: str, action: str = "typing") -> dict[str, Any]:
        data = self._api_post("sendChatAction", {"chat_id": channel_id, "action": action})
        return {"ok": bool(data.get("ok")), "provider": self.provider, "raw": data}

    def get_chat(self, chat_id: str) -> dict[str, Any]:
        data = self._api_post("getChat", {"chat_id": str(chat_id)})
        result = data.get("result", {}) if isinstance(data, dict) else {}
        if hasattr(result, "to_dict"):
            data = result.to_dict()
            return data if isinstance(data, dict) else {}
        return {}

    def answer_callback_query(
        self, callback_query_id: str, text: Optional[str] = None, show_alert: bool = False
    ) -> dict[str, Any]:
        payload: dict[str, Any] = {"callback_query_id": callback_query_id, "show_alert": bool(show_alert)}
        if text:
            payload["text"] = text
        data = self._api_post("answerCallbackQuery", payload)
        return {"ok": bool(data.get("ok")), "provider": self.provider, "raw": data}

    def set_my_commands(self, commands: list[dict[str, str]]) -> dict[str, Any]:
        payload = {
            "commands": [
                BotCommand(command=str(item.get("command") or "").strip(), description=str(item.get("description") or "").strip())
                for item in (commands or [])
                if str(item.get("command") or "").strip() and str(item.get("description") or "").strip()
            ]
        }
        data = self._api_post("setMyCommands", payload)
        return {"ok": bool(data.get("ok")), "provider": self.provider, "raw": data}

    def get_my_commands(self) -> list[dict[str, str]]:
        data = self._api_post("getMyCommands", {})
        result = data.get("result", []) if isinstance(data, dict) else []
        if not isinstance(result, list):
            return []
        out: list[dict[str, str]] = []
        for item in result:
            if hasattr(item, "to_dict"):
                item = item.to_dict()
            if isinstance(item, dict):
                command = str(item.get("command") or "").strip()
                description = str(item.get("description") or "").strip()
                if command and description:
                    out.append({"command": command, "description": description})
        return out

    def normalize_event(self, payload: Mapping[str, Any]) -> Optional[MessagingEvent]:
        update = dict(payload)
        callback = update.get("callback_query")
        if isinstance(callback, dict):
            message = callback.get("message") if isinstance(callback.get("message"), dict) else {}
            chat = message.get("chat") if isinstance(message.get("chat"), dict) else {}
            sender = callback.get("from") if isinstance(callback.get("from"), dict) else {}
            chat_id = chat.get("id")
            user_id = sender.get("id")
            if chat_id is None or user_id is None:
                return None
            return MessagingEvent(
                provider=self.provider,
                event_id=str(update.get("update_id") or callback.get("id") or ""),
                channel_id=str(chat_id),
                user_id=str(user_id),
                text=str(callback.get("data") or ""),
                message_id=str(message.get("message_id")) if message.get("message_id") is not None else None,
                timestamp=int(callback["message"]["date"])
                if isinstance(callback.get("message"), dict)
                and isinstance(callback["message"].get("date"), int)
                else None,
                raw=update,
            )

        message = (
            update.get("message")
            or update.get("edited_message")
            or update.get("channel_post")
            or update.get("edited_channel_post")
        )
        if not isinstance(message, dict):
            return None

        chat = message.get("chat", {}) if isinstance(message.get("chat"), dict) else {}
        sender = message.get("from", {}) if isinstance(message.get("from"), dict) else {}

        chat_id = chat.get("id")
        user_id = sender.get("id")
        if chat_id is None or user_id is None:
            return None

        event_id = str(update.get("update_id") or message.get("message_id") or "")
        text = message.get("text") or message.get("caption") or ""

        return MessagingEvent(
            provider=self.provider,
            event_id=event_id,
            channel_id=str(chat_id),
            user_id=str(user_id),
            text=str(text),
            message_id=str(message.get("message_id")) if message.get("message_id") is not None else None,
            timestamp=int(message["date"]) if isinstance(message.get("date"), int) else None,
            raw=update,
        )

    def map_user(self, event: MessagingEvent) -> UserMapping:
        message = event.raw.get("message") or event.raw.get("edited_message") or {}
        sender = message.get("from", {}) if isinstance(message, dict) else {}

        username = sender.get("username") if isinstance(sender, dict) else None
        first = sender.get("first_name") if isinstance(sender, dict) else None
        last = sender.get("last_name") if isinstance(sender, dict) else None
        display_name = " ".join([n for n in [first, last] if n]) or None

        return UserMapping(
            provider=self.provider,
            external_user_id=event.user_id,
            external_channel_id=event.channel_id,
            username=username,
            display_name=display_name,
        )
