from __future__ import annotations

import json
import os
import re
import uuid
from pathlib import Path
from typing import Any, Mapping, Optional
from urllib import error, request

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

    def _api_url(self, method: str) -> str:
        return f"{self.api_base}/bot{self.bot_token}/{method}"

    def _api_post(self, method: str, payload: Mapping[str, Any], timeout: Optional[int] = None) -> dict[str, Any]:
        body = json.dumps(dict(payload)).encode("utf-8")
        req = request.Request(
            self._api_url(method),
            data=body,
            headers={"Content-Type": "application/json"},
            method="POST",
        )
        request_timeout = int(timeout) if timeout is not None else self.timeout
        try:
            with request.urlopen(req, timeout=request_timeout) as resp:
                data = json.loads(resp.read().decode("utf-8") or "{}")
        except error.HTTPError as exc:
            err_body = exc.read().decode("utf-8", errors="ignore")
            raise RuntimeError(f"Telegram API HTTP {exc.code}: {err_body}") from exc
        except error.URLError as exc:
            raise RuntimeError(f"Telegram API connection error: {exc.reason}") from exc

        if not data.get("ok", False):
            raise RuntimeError(f"Telegram API error: {data}")
        return data

    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]:
        boundary = f"----armorer-{uuid.uuid4().hex}"
        body = bytearray()

        def _add_line(line: str) -> None:
            body.extend(line.encode("utf-8"))
            body.extend(b"\r\n")

        for k, v in fields.items():
            _add_line(f"--{boundary}")
            _add_line(f'Content-Disposition: form-data; name="{k}"')
            _add_line("")
            _add_line(str(v))

        _add_line(f"--{boundary}")
        _add_line(
            f'Content-Disposition: form-data; name="{file_field}"; filename="{file_path.name}"'
        )
        _add_line(f"Content-Type: {content_type}")
        _add_line("")
        body.extend(file_path.read_bytes())
        body.extend(b"\r\n")
        _add_line(f"--{boundary}--")

        req = request.Request(
            self._api_url(method),
            data=bytes(body),
            headers={"Content-Type": f"multipart/form-data; boundary={boundary}"},
            method="POST",
        )
        request_timeout = int(timeout) if timeout is not None else self.timeout
        try:
            with request.urlopen(req, timeout=request_timeout) as resp:
                data = json.loads(resp.read().decode("utf-8") or "{}")
        except error.HTTPError as exc:
            err_body = exc.read().decode("utf-8", errors="ignore")
            raise RuntimeError(f"Telegram API HTTP {exc.code}: {err_body}") from exc
        except error.URLError as exc:
            raise RuntimeError(f"Telegram API connection error: {exc.reason}") from exc

        if not data.get("ok", False):
            raise RuntimeError(f"Telegram API error: {data}")
        return data

    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 result.get("message_id") is not None else None,
            "raw": data,
        }

    def get_me(self) -> dict[str, Any]:
        data = self._api_post("getMe", {})
        result = data.get("result", {}) if isinstance(data, dict) else {}
        return result if isinstance(result, 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 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_me(self) -> dict[str, Any]:
        data = self._api_post("getMe", {})
        result = data.get("result", {}) if isinstance(data, dict) else {}
        return result if isinstance(result, dict) else {}

    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 {}
        return result if isinstance(result, dict) else {}

    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 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,
        )
