"""
КругСвоих Bot SDK for Python
https://krugsvoih.ru/bots.html

No external dependencies — uses only the Python standard library.
Requires Python 3.8+

Quick start:
    from ks_bot import KSBot

    bot = KSBot("YOUR_TOKEN")

    @bot.on_command("start")
    def on_start(msg):
        bot.send_message(msg["chatId"], "Привет! 👋")

    bot.run_polling()
"""

import json
import mimetypes
import os
import time
import threading
import urllib.request
import urllib.error
from typing import Any, Callable, Dict, List, Optional


BASE_URL = "https://v.krugsvoih.ru/bot"


class BotError(Exception):
    def __init__(self, description: str):
        super().__init__(description)
        self.description = description


class KSBot:
    """КругСвоих Bot API client."""

    def __init__(self, token: str):
        self.token = token
        self._base = f"{BASE_URL}/{token}"
        self._offset: int = 0
        self._running = False
        self._message_handlers: List[Callable] = []
        self._command_handlers: Dict[str, Callable] = {}
        self._callback_handlers: List[Callable] = []

    # ── Low-level requests ─────────────────────────────────────────────────────

    def _request(self, method: str, data: Optional[dict] = None) -> Any:
        url = f"{self._base}/{method}"
        body = json.dumps(data).encode("utf-8") if data is not None else None
        req = urllib.request.Request(
            url, data=body,
            headers={"Content-Type": "application/json"},
            method="POST" if body is not None else "GET",
        )
        try:
            with urllib.request.urlopen(req, timeout=35) as resp:
                result = json.loads(resp.read().decode("utf-8"))
                if not result.get("ok"):
                    raise BotError(result.get("description", "Unknown error"))
                return result["result"]
        except urllib.error.HTTPError as e:
            try:
                result = json.loads(e.read().decode("utf-8"))
                raise BotError(result.get("description", str(e)))
            except (json.JSONDecodeError, AttributeError):
                raise BotError(str(e))

    def _upload_request(self, method: str, file_path: str, file_field: str, fields: dict) -> Any:
        url = f"{self._base}/{method}"
        boundary = f"KSBotBoundary{int(time.time() * 1000)}"
        mime_type = mimetypes.guess_type(file_path)[0] or "application/octet-stream"
        filename = os.path.basename(file_path)
        with open(file_path, "rb") as f:
            file_bytes = f.read()
        parts = []
        for name, value in fields.items():
            if value is not None:
                parts.append(
                    f'--{boundary}\r\nContent-Disposition: form-data; name="{name}"\r\n\r\n{value}\r\n'.encode()
                )
        parts.append(
            f'--{boundary}\r\nContent-Disposition: form-data; name="{file_field}"; filename="{filename}"\r\nContent-Type: {mime_type}\r\n\r\n'.encode()
            + file_bytes + b'\r\n'
        )
        parts.append(f'--{boundary}--\r\n'.encode())
        body = b''.join(parts)
        req = urllib.request.Request(
            url, data=body,
            headers={"Content-Type": f"multipart/form-data; boundary={boundary}"},
            method="POST",
        )
        try:
            with urllib.request.urlopen(req, timeout=60) as resp:
                result = json.loads(resp.read().decode("utf-8"))
                if not result.get("ok"):
                    raise BotError(result.get("description", "Unknown error"))
                return result["result"]
        except urllib.error.HTTPError as e:
            try:
                result = json.loads(e.read().decode("utf-8"))
                raise BotError(result.get("description", str(e)))
            except (json.JSONDecodeError, AttributeError):
                raise BotError(str(e))

    # ── Bot info ───────────────────────────────────────────────────────────────

    def get_me(self) -> dict:
        """Returns basic information about the bot."""
        return self._request("getMe")

    # ── Sending messages ───────────────────────────────────────────────────────

    def send_message(
        self,
        chat_id: str,
        text: str,
        reply_to_message_id: Optional[str] = None,
        reply_markup: Optional[dict] = None,
    ) -> dict:
        """Send a text message. Returns the sent Message."""
        payload: dict = {"chatId": chat_id, "text": text}
        if reply_to_message_id:
            payload["replyToMessageId"] = reply_to_message_id
        if reply_markup:
            payload["replyMarkup"] = reply_markup
        return self._request("sendMessage", payload)

    def send_photo(
        self,
        chat_id: str,
        photo_url: str,
        caption: Optional[str] = None,
        reply_markup: Optional[dict] = None,
    ) -> dict:
        """Send a photo by URL with optional caption and inline keyboard."""
        payload: dict = {"chatId": chat_id, "photoUrl": photo_url}
        if caption:
            payload["caption"] = caption
        if reply_markup:
            payload["replyMarkup"] = reply_markup
        return self._request("sendPhoto", payload)

    def send_document(
        self,
        chat_id: str,
        document_url: str,
        file_name: str,
        caption: Optional[str] = None,
        reply_markup: Optional[dict] = None,
    ) -> dict:
        """Send a document by URL."""
        payload: dict = {"chatId": chat_id, "documentUrl": document_url, "fileName": file_name}
        if caption:
            payload["caption"] = caption
        if reply_markup:
            payload["replyMarkup"] = reply_markup
        return self._request("sendDocument", payload)

    def upload_photo(
        self,
        chat_id: str,
        file_path: str,
        caption: Optional[str] = None,
    ) -> dict:
        """Upload and send a photo file directly (no external hosting needed)."""
        fields: dict = {"chatId": chat_id}
        if caption:
            fields["caption"] = caption
        return self._upload_request("sendPhoto/upload", file_path, "photo", fields)

    def upload_document(
        self,
        chat_id: str,
        file_path: str,
        caption: Optional[str] = None,
    ) -> dict:
        """Upload and send any file directly (no external hosting needed)."""
        fields: dict = {"chatId": chat_id}
        if caption:
            fields["caption"] = caption
        return self._upload_request("sendDocument/upload", file_path, "document", fields)

    def forward_message(self, from_chat_id: str, message_id: str, to_chat_id: str) -> dict:
        """Forward a message from one chat to another."""
        return self._request("forwardMessage", {
            "fromChatId": from_chat_id,
            "messageId": message_id,
            "toChatId": to_chat_id,
        })

    # ── Editing and deleting ───────────────────────────────────────────────────

    def edit_message_text(self, chat_id: str, message_id: str, sent_at: str, text: str) -> dict:
        """Edit text of a previously sent message."""
        return self._request("editMessageText", {
            "chatId": chat_id,
            "messageId": message_id,
            "sentAt": sent_at,
            "text": text,
        })

    def delete_message(self, chat_id: str, message_id: str, sent_at: str) -> bool:
        """Delete a message."""
        return self._request("deleteMessage", {
            "chatId": chat_id,
            "messageId": message_id,
            "sentAt": sent_at,
        })

    # ── Chat management ────────────────────────────────────────────────────────

    def get_chat(self, chat_id: str) -> dict:
        """Get information about a chat."""
        return self._request("getChat", {"chatId": chat_id})

    def get_chat_members(self, chat_id: str) -> list:
        """Get all members of a chat."""
        return self._request("getChatMembers", {"chatId": chat_id})

    def get_chat_member(self, chat_id: str, user_id: str) -> dict:
        """Get information about a specific chat member."""
        return self._request("getChatMember", {"chatId": chat_id, "userId": user_id})

    def ban_chat_member(self, chat_id: str, user_id: str) -> bool:
        """Ban a user from a chat."""
        return self._request("banChatMember", {"chatId": chat_id, "userId": user_id})

    def unban_chat_member(self, chat_id: str, user_id: str) -> bool:
        """Unban a previously banned user."""
        return self._request("unbanChatMember", {"chatId": chat_id, "userId": user_id})

    def leave_chat(self, chat_id: str) -> bool:
        """Make the bot leave a chat."""
        return self._request("leaveChat", {"chatId": chat_id})

    # ── Pinning messages ───────────────────────────────────────────────────────

    def pin_message(self, chat_id: str, message_id: str, sent_at: str) -> bool:
        """Pin a message in a chat."""
        return self._request("pinChatMessage", {
            "chatId": chat_id,
            "messageId": message_id,
            "sentAt": sent_at,
        })

    def unpin_message(self, chat_id: str) -> bool:
        """Unpin the current pinned message in a chat."""
        return self._request("unpinChatMessage", {"chatId": chat_id})

    # ── Updates ────────────────────────────────────────────────────────────────

    def get_updates(self, offset: int = 0, limit: int = 100, timeout: int = 0) -> list:
        """
        Get incoming updates using long polling.
        timeout > 0 enables long polling (recommended: 20-30 seconds).
        """
        return self._request("getUpdates", {"offset": offset, "limit": limit, "timeout": timeout})

    # ── Webhooks ───────────────────────────────────────────────────────────────

    def set_webhook(self, url: str) -> bool:
        """Set a URL to receive updates via webhook."""
        return self._request("setWebhook", {"url": url})

    def delete_webhook(self) -> bool:
        """Remove the webhook and switch back to long polling."""
        return self._request("deleteWebhook")

    def get_webhook_info(self) -> dict:
        """Get current webhook status."""
        return self._request("getWebhookInfo")

    # ── Commands ───────────────────────────────────────────────────────────────

    def set_my_commands(self, commands: List[Dict[str, str]]) -> bool:
        """
        Set the list of bot commands.
        Each command: {"command": "/start", "description": "Start the bot"}
        """
        return self._request("setMyCommands", {"commands": commands})

    def get_my_commands(self) -> list:
        """Get the current list of bot commands."""
        return self._request("getMyCommands")

    # ── Callback queries ───────────────────────────────────────────────────────

    def answer_callback_query(
        self,
        callback_query_id: str,
        text: Optional[str] = None,
        show_alert: bool = False,
    ) -> bool:
        """Answer an inline keyboard callback query."""
        payload: dict = {"callbackQueryId": callback_query_id, "showAlert": show_alert}
        if text:
            payload["text"] = text
        return self._request("answerCallbackQuery", payload)

    # ── Keyboard helpers ───────────────────────────────────────────────────────

    @staticmethod
    def inline_keyboard(*rows: list) -> dict:
        """
        Build an inline keyboard markup.

        Example:
            markup = KSBot.inline_keyboard(
                [KSBot.button("✅ Yes", callback_data="yes"),
                 KSBot.button("❌ No",  callback_data="no")],
                [KSBot.button("🌐 Visit", url="https://krugsvoih.ru")],
            )
        """
        return {"inlineKeyboard": list(rows)}

    @staticmethod
    def button(
        text: str,
        callback_data: Optional[str] = None,
        url: Optional[str] = None,
    ) -> dict:
        """Create an inline keyboard button."""
        btn: dict = {"text": text}
        if callback_data is not None:
            btn["callbackData"] = callback_data
        if url is not None:
            btn["url"] = url
        return btn

    # ── Handler decorators ─────────────────────────────────────────────────────

    def on_message(self, func: Optional[Callable] = None):
        """
        Register a handler for all non-command messages.

        Usage as decorator:
            @bot.on_message
            def handler(msg): ...
        """
        def decorator(f: Callable) -> Callable:
            self._message_handlers.append(f)
            return f
        return decorator(func) if func is not None else decorator

    def on_command(self, command: str):
        """
        Register a handler for a specific bot command.

        Usage:
            @bot.on_command("start")
            def handler(msg): ...
        """
        def decorator(f: Callable) -> Callable:
            self._command_handlers[command.lstrip("/")] = f
            return f
        return decorator

    def on_callback(self, func: Optional[Callable] = None):
        """
        Register a handler for inline keyboard callbacks.

        Usage as decorator:
            @bot.on_callback
            def handler(cb): ...
        """
        def decorator(f: Callable) -> Callable:
            self._callback_handlers.append(f)
            return f
        return decorator(func) if func is not None else decorator

    # ── Dispatch ───────────────────────────────────────────────────────────────

    def _dispatch(self, update: dict) -> None:
        msg = update.get("message")
        cb = update.get("callbackQuery")

        if msg:
            text = msg.get("content", "") or ""
            if text.startswith("/"):
                parts = text.split()
                cmd = parts[0].lstrip("/").split("@")[0].lower()
                handler = self._command_handlers.get(cmd)
                if handler:
                    handler(msg)
                    return
            for handler in self._message_handlers:
                handler(msg)

        if cb:
            for handler in self._callback_handlers:
                handler(cb)

    # ── Polling loop ───────────────────────────────────────────────────────────

    def run_polling(self, poll_timeout: int = 30, error_sleep: float = 5.0) -> None:
        """
        Start long-polling loop (blocking). Press Ctrl+C to stop.

        poll_timeout: seconds to wait for updates on the server side (long polling).
        """
        me = self.get_me()
        print(f"Bot @{me.get('username')} started. Press Ctrl+C to stop.")
        self._running = True
        while self._running:
            try:
                updates = self.get_updates(offset=self._offset, timeout=poll_timeout)
                for update in updates:
                    try:
                        self._dispatch(update)
                    except Exception as e:
                        print(f"[handler error] {e}")
                    self._offset = update.get("updateId", self._offset) + 1
            except BotError as e:
                print(f"[bot error] {e}")
                time.sleep(error_sleep)
            except KeyboardInterrupt:
                print("Bot stopped.")
                self._running = False
            except Exception as e:
                print(f"[error] {e}")
                time.sleep(error_sleep)

    def stop(self) -> None:
        """Stop the polling loop (thread-safe)."""
        self._running = False
