"""
High-level bot framework with decorator-based event routing.

Inspired by python-telegram-bot and discord.py, this provides a simple
way to build bots with @bot.on_command, @bot.on_callback, @bot.on_message
decorators.

Usage::

    from dexgram import Bot

    bot = Bot(ws_url="ws://localhost:5225")

    @bot.on_contact_connected
    async def welcome(contact):
        await bot.send_text(contact.id, "Welcome!")

    @bot.on_command("help")
    async def help_cmd(contact, args):
        await bot.send_text(contact.id, "Available commands: /help, /catalog")

    @bot.on_callback("buy:*")
    async def buy(contact, callback):
        product_id = callback.data.split(":")[1]
        await bot.send_text(contact.id, f"Buying {product_id}...")

    @bot.on_message
    async def fallback(contact, text):
        await bot.send_text(contact.id, "I don't understand. Try /help")

    bot.run()
"""

from __future__ import annotations

import asyncio
import fnmatch
import logging
import signal
import uuid
from typing import Any, Callable, Awaitable

from .client import DexgramClient, DexgramCommandError
from .types import ChatType, Contact, MsgContent, ComposedMessage
from .rich_messages import (
    MSG_PREFIX,
    CB_PREFIX,
    BotCallbackQuery,
    BotMessage,
    InlineButton,
    ProductCard,
    FormField,
    BotForm,
    text_with_buttons,
    product_card as _product_card,
    catalog as _catalog,
    form as _form,
    receipt as _receipt,
    media as _media,
)

logger = logging.getLogger("dexgram.bot")

ContactHandler = Callable[[Contact], Awaitable[None]]
MessageHandler = Callable[[Contact, str], Awaitable[None]]
CommandHandler = Callable[[Contact, str], Awaitable[None]]
CallbackHandler = Callable[[Contact, BotCallbackQuery], Awaitable[None]]


class Bot:
    """
    High-level bot framework.

    Connects to SimpleX Chat CLI, sets up the bot profile,
    and routes incoming events to registered handlers.
    """

    def __init__(
        self,
        ws_url: str = "ws://localhost:5225",
        bot_name: str | None = None,
        auto_accept: bool = True,
        welcome_message: str | None = None,
    ):
        self.ws_url = ws_url
        self.bot_name = bot_name
        self.auto_accept = auto_accept
        self.welcome_message = welcome_message

        self._client: DexgramClient | None = None
        self._user_id: int | None = None

        self._on_contact_connected_handlers: list[ContactHandler] = []
        self._on_contact_disconnected_handlers: list[ContactHandler] = []
        self._command_handlers: dict[str, CommandHandler] = {}
        self._callback_handlers: list[tuple[str, CallbackHandler]] = []
        self._message_handlers: list[MessageHandler] = []
        self._on_start_handlers: list[Callable[[], Awaitable[None]]] = []

    @property
    def client(self) -> DexgramClient:
        if self._client is None:
            raise RuntimeError("Bot is not connected. Call bot.run() or bot.start() first.")
        return self._client

    # ── Decorators ──

    def on_start(self, func: Callable[[], Awaitable[None]]) -> Callable[[], Awaitable[None]]:
        self._on_start_handlers.append(func)
        return func

    def on_contact_connected(self, func: ContactHandler) -> ContactHandler:
        self._on_contact_connected_handlers.append(func)
        return func

    def on_contact_disconnected(self, func: ContactHandler) -> ContactHandler:
        self._on_contact_disconnected_handlers.append(func)
        return func

    def on_command(self, keyword: str) -> Callable[[CommandHandler], CommandHandler]:
        def decorator(func: CommandHandler) -> CommandHandler:
            self._command_handlers[keyword.lower()] = func
            return func
        return decorator

    def on_callback(self, pattern: str) -> Callable[[CallbackHandler], CallbackHandler]:
        """
        Register a callback handler. Pattern supports wildcards.

        Examples::
            @bot.on_callback("browse")       # exact match
            @bot.on_callback("buy:*")         # wildcard match
            @bot.on_callback("order:*:*")     # multi-segment wildcard
        """
        def decorator(func: CallbackHandler) -> CallbackHandler:
            self._callback_handlers.append((pattern, func))
            return func
        return decorator

    def on_message(self, func: MessageHandler) -> MessageHandler:
        self._message_handlers.append(func)
        return func

    # ── Send helpers ──

    async def send_text(self, contact_id: int, text: str) -> None:
        await self.client.api_send_text(ChatType.DIRECT, contact_id, text)

    async def send_inline_keyboard(
        self,
        contact_id: int,
        text: str,
        keyboard: list[list[dict[str, Any] | InlineButton]],
    ) -> None:
        encoded = text_with_buttons(text, keyboard, callback_id=str(uuid.uuid4()))
        await self.client.api_send_text(ChatType.DIRECT, contact_id, encoded)

    async def send_product_card(
        self,
        contact_id: int,
        *,
        id: str,
        title: str,
        price: str,
        currency: str,
        description: str | None = None,
        image_url: str | None = None,
        network: str | None = None,
        token_symbol: str | None = None,
        wallet_address: str | None = None,
        keyboard: list[list[dict[str, Any] | InlineButton]] | None = None,
    ) -> None:
        encoded = _product_card(
            id=id, title=title, price=price, currency=currency,
            description=description, image_url=image_url,
            network=network, token_symbol=token_symbol,
            wallet_address=wallet_address, keyboard=keyboard,
        )
        await self.client.api_send_text(ChatType.DIRECT, contact_id, encoded)

    async def send_catalog(
        self,
        contact_id: int,
        products: list[dict[str, Any] | ProductCard],
        text: str | None = None,
    ) -> None:
        encoded = _catalog(products, text=text)
        await self.client.api_send_text(ChatType.DIRECT, contact_id, encoded)

    async def send_form(
        self,
        contact_id: int,
        *,
        id: str,
        title: str,
        fields: list[dict[str, Any] | FormField],
        submit_label: str = "Submit",
        callback_data: str | None = None,
        text: str | None = None,
    ) -> None:
        encoded = _form(
            id=id, title=title, fields=fields,
            submit_label=submit_label, callback_data=callback_data, text=text,
        )
        await self.client.api_send_text(ChatType.DIRECT, contact_id, encoded)

    async def send_receipt(
        self,
        contact_id: int,
        text: str,
        tx_hash: str | None = None,
        keyboard: list[list[dict[str, Any] | InlineButton]] | None = None,
    ) -> None:
        encoded = _receipt(text, tx_hash=tx_hash, keyboard=keyboard)
        await self.client.api_send_text(ChatType.DIRECT, contact_id, encoded)

    async def send_media(
        self,
        contact_id: int,
        url: str,
        caption: str | None = None,
        keyboard: list[list[dict[str, Any] | InlineButton]] | None = None,
    ) -> None:
        encoded = _media(url, caption=caption, keyboard=keyboard)
        await self.client.api_send_text(ChatType.DIRECT, contact_id, encoded)

    async def send_payment_request(
        self,
        contact_id: int,
        amount: str,
        currency: str,
        network: str,
        wallet_address: str,
        token_symbol: str | None = None,
        memo: str | None = None,
    ) -> None:
        """Send a payment invoice using the existing wallet payment protocol."""
        invoice = {
            "invoiceId": str(uuid.uuid4()),
            "network": network,
            "amount": amount,
            "tokenSymbol": token_symbol or currency,
            "recipientAddress": wallet_address,
            "status": "PENDING",
        }
        if memo:
            invoice["memo"] = memo
        import json
        encoded = "__PAY_INV__" + json.dumps(invoice)
        await self.client.api_send_text(ChatType.DIRECT, contact_id, encoded)

    # ── Group send helpers ──

    async def send_group_text(self, group_id: int, text: str) -> None:
        await self.client.api_send_text(ChatType.GROUP, group_id, text)

    async def send_group_inline_keyboard(
        self,
        group_id: int,
        text: str,
        keyboard: list[list[dict[str, Any] | InlineButton]],
    ) -> None:
        encoded = text_with_buttons(text, keyboard, callback_id=str(uuid.uuid4()))
        await self.client.api_send_text(ChatType.GROUP, group_id, encoded)

    # ── Event processing ──

    async def _handle_event(self, event: dict[str, Any]) -> None:
        event_type = event.get("type", "")

        try:
            if event_type == "contactConnected":
                contact = Contact.from_dict(event.get("contact", {}))
                if self.welcome_message:
                    await self.send_text(contact.id, self.welcome_message)
                for handler in self._on_contact_connected_handlers:
                    await handler(contact)

            elif event_type == "contactDisconnected":
                contact = Contact.from_dict(event.get("contact", {}))
                for handler in self._on_contact_disconnected_handlers:
                    await handler(contact)

            elif event_type == "newChatItems":
                for item_data in event.get("chatItems", []):
                    await self._handle_chat_item(item_data)

        except Exception as e:
            logger.error("Error handling event %s: %s", event_type, e, exc_info=True)

    async def _handle_chat_item(self, item_data: dict[str, Any]) -> None:
        chat_info = item_data.get("chatInfo", {})
        if chat_info.get("type") != "direct":
            return

        contact = Contact.from_dict(chat_info.get("contact", {}))
        chat_item = item_data.get("chatItem", {})
        content = chat_item.get("content", {})

        if content.get("type") not in ("rcvMsgContent",):
            return

        msg_content = content.get("msgContent", {})
        if msg_content.get("type") != "text":
            return

        text = msg_content.get("text", "").strip()
        if not text:
            return

        # Callback query from user clicking a button
        if text.startswith(CB_PREFIX):
            callback = BotCallbackQuery.decode(text)
            if callback:
                await self._route_callback(contact, callback)
            return

        # Command (starts with /)
        if text.startswith("/"):
            parts = text[1:].split(None, 1)
            keyword = parts[0].lower() if parts else ""
            args = parts[1] if len(parts) > 1 else ""
            if keyword in self._command_handlers:
                await self._command_handlers[keyword](contact, args)
                return

        # Plain text message
        for handler in self._message_handlers:
            await handler(contact, text)

    async def _route_callback(self, contact: Contact, callback: BotCallbackQuery) -> None:
        for pattern, handler in self._callback_handlers:
            if fnmatch.fnmatch(callback.data, pattern):
                await handler(contact, callback)
                return
        logger.debug("No handler for callback data: %s", callback.data)

    # ── Lifecycle ──

    async def start(self) -> None:
        logger.info("Connecting to %s ...", self.ws_url)
        self._client = await DexgramClient.create(self.ws_url)

        user = await self.client.api_get_active_user()
        if user is None:
            raise RuntimeError(
                "No active user profile. Create one first with:\n"
                "  simplex-chat --create-bot-display-name 'MyBot'"
            )
        self._user_id = user.id
        logger.info("Bot profile: %s (ID: %d)", user.local_display_name, user.id)

        if self.auto_accept:
            try:
                await self.client.enable_address_auto_accept(user.id)
                logger.info("Auto-accept enabled")
            except DexgramCommandError:
                logger.warning("Could not enable auto-accept (address may not exist yet)")

        address = None
        try:
            address = await self.client.api_get_user_address(user.id)
        except DexgramCommandError:
            pass

        if address is None:
            try:
                address = await self.client.api_create_user_address(user.id)
                if self.auto_accept:
                    await self.client.enable_address_auto_accept(user.id)
            except DexgramCommandError:
                logger.warning("Could not create bot address")

        if address:
            logger.info("Bot address: %s", address)

        for handler in self._on_start_handlers:
            await handler()

    async def poll(self) -> None:
        logger.info("Listening for events...")
        async for event in self.client.events:
            await self._handle_event(event)

    async def stop(self) -> None:
        if self._client:
            await self._client.disconnect()
            self._client = None
        logger.info("Bot stopped")

    def run(self) -> None:
        """Start the bot and block until interrupted."""

        async def _run() -> None:
            await self.start()
            try:
                await self.poll()
            except asyncio.CancelledError:
                pass
            finally:
                await self.stop()

        try:
            asyncio.run(_run())
        except KeyboardInterrupt:
            logger.info("Bot interrupted")
