"""
Rich bot message encoding/decoding for the Dexgram protocol.

Bot messages are encoded as MCText with a special prefix so the
Dark Link client can detect and render them with rich UI components
(inline keyboards, product cards, forms, catalogs).

Protocol:
  - __BOT_MSG__ + JSON  →  Rich bot message (bot → user)
  - __BOT_CB__  + JSON  →  Callback query    (user → bot)
"""

from __future__ import annotations

import json
from dataclasses import dataclass, field, asdict
from enum import Enum
from typing import Any


MSG_PREFIX = "__BOT_MSG__"
CB_PREFIX = "__BOT_CB__"


class BotMessageType(str, Enum):
    TEXT = "TEXT"
    PRODUCT = "PRODUCT"
    CATALOG = "CATALOG"
    FORM = "FORM"
    MEDIA = "MEDIA"
    RECEIPT = "RECEIPT"


class FormFieldType(str, Enum):
    TEXT = "text"
    NUMBER = "number"
    EMAIL = "email"
    PHONE = "phone"
    DROPDOWN = "dropdown"
    TEXTAREA = "textarea"


# ── Inline Keyboard ──

@dataclass
class InlineButton:
    text: str
    callback_data: str | None = None
    url: str | None = None
    pay: bool = False

    def to_dict(self) -> dict[str, Any]:
        d: dict[str, Any] = {"text": self.text}
        if self.callback_data is not None:
            d["callbackData"] = self.callback_data
        if self.url is not None:
            d["url"] = self.url
        if self.pay:
            d["pay"] = True
        return d

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> InlineButton:
        return cls(
            text=data.get("text", ""),
            callback_data=data.get("callbackData"),
            url=data.get("url"),
            pay=data.get("pay", False),
        )


# ── Product Card ──

@dataclass
class ProductCard:
    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

    def to_dict(self) -> dict[str, Any]:
        d: dict[str, Any] = {
            "id": self.id,
            "title": self.title,
            "price": self.price,
            "currency": self.currency,
        }
        if self.description is not None:
            d["description"] = self.description
        if self.image_url is not None:
            d["imageUrl"] = self.image_url
        if self.network is not None:
            d["network"] = self.network
        if self.token_symbol is not None:
            d["tokenSymbol"] = self.token_symbol
        if self.wallet_address is not None:
            d["walletAddress"] = self.wallet_address
        return d

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> ProductCard:
        return cls(
            id=data.get("id", ""),
            title=data.get("title", ""),
            price=data.get("price", "0"),
            currency=data.get("currency", ""),
            description=data.get("description"),
            image_url=data.get("imageUrl"),
            network=data.get("network"),
            token_symbol=data.get("tokenSymbol"),
            wallet_address=data.get("walletAddress"),
        )


# ── Form ──

@dataclass
class FormField:
    name: str
    label: str
    field_type: FormFieldType = FormFieldType.TEXT
    required: bool = True
    placeholder: str | None = None
    options: list[str] | None = None

    def to_dict(self) -> dict[str, Any]:
        d: dict[str, Any] = {
            "name": self.name,
            "label": self.label,
            "fieldType": self.field_type.value,
            "required": self.required,
        }
        if self.placeholder is not None:
            d["placeholder"] = self.placeholder
        if self.options is not None:
            d["options"] = self.options
        return d

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> FormField:
        return cls(
            name=data.get("name", ""),
            label=data.get("label", ""),
            field_type=FormFieldType(data.get("fieldType", "text")),
            required=data.get("required", True),
            placeholder=data.get("placeholder"),
            options=data.get("options"),
        )


@dataclass
class BotForm:
    id: str
    title: str
    fields: list[FormField]
    submit_label: str = "Submit"
    callback_data: str | None = None

    def to_dict(self) -> dict[str, Any]:
        return {
            "id": self.id,
            "title": self.title,
            "fields": [f.to_dict() for f in self.fields],
            "submitLabel": self.submit_label,
            "callbackData": self.callback_data,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> BotForm:
        return cls(
            id=data.get("id", ""),
            title=data.get("title", ""),
            fields=[FormField.from_dict(f) for f in data.get("fields", [])],
            submit_label=data.get("submitLabel", "Submit"),
            callback_data=data.get("callbackData"),
        )


# ── Bot Message (the main envelope) ──

@dataclass
class BotMessage:
    type: BotMessageType
    text: str | None = None
    keyboard: list[list[InlineButton]] | None = None
    product_card: ProductCard | None = None
    products: list[ProductCard] | None = None
    form: BotForm | None = None
    media_url: str | None = None
    media_caption: str | None = None
    callback_id: str | None = None

    def to_dict(self) -> dict[str, Any]:
        d: dict[str, Any] = {"type": self.type.value}
        if self.text is not None:
            d["text"] = self.text
        if self.keyboard is not None:
            d["keyboard"] = [[btn.to_dict() for btn in row] for row in self.keyboard]
        if self.product_card is not None:
            d["productCard"] = self.product_card.to_dict()
        if self.products is not None:
            d["products"] = [p.to_dict() for p in self.products]
        if self.form is not None:
            d["form"] = self.form.to_dict()
        if self.media_url is not None:
            d["mediaUrl"] = self.media_url
        if self.media_caption is not None:
            d["mediaCaption"] = self.media_caption
        if self.callback_id is not None:
            d["callbackId"] = self.callback_id
        return d

    def encode(self) -> str:
        return MSG_PREFIX + json.dumps(self.to_dict(), separators=(",", ":"))

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> BotMessage:
        keyboard = None
        if data.get("keyboard"):
            keyboard = [
                [InlineButton.from_dict(btn) for btn in row]
                for row in data["keyboard"]
            ]
        return cls(
            type=BotMessageType(data.get("type", "TEXT")),
            text=data.get("text"),
            keyboard=keyboard,
            product_card=ProductCard.from_dict(data["productCard"]) if data.get("productCard") else None,
            products=[ProductCard.from_dict(p) for p in data["products"]] if data.get("products") else None,
            form=BotForm.from_dict(data["form"]) if data.get("form") else None,
            media_url=data.get("mediaUrl"),
            media_caption=data.get("mediaCaption"),
            callback_id=data.get("callbackId"),
        )

    @classmethod
    def decode(cls, text: str) -> BotMessage | None:
        if not text.startswith(MSG_PREFIX):
            return None
        try:
            data = json.loads(text[len(MSG_PREFIX):])
            return cls.from_dict(data)
        except (json.JSONDecodeError, KeyError, ValueError):
            return None

    @staticmethod
    def is_bot_message(text: str) -> bool:
        return text.startswith(MSG_PREFIX)

    @staticmethod
    def is_callback(text: str) -> bool:
        return text.startswith(CB_PREFIX)

    @staticmethod
    def is_bot_protocol(text: str) -> bool:
        return text.startswith(MSG_PREFIX) or text.startswith(CB_PREFIX)

    @staticmethod
    def human_readable(text: str) -> str:
        if text.startswith(MSG_PREFIX):
            msg = BotMessage.decode(text)
            if msg is None:
                return "Bot Message"
            if msg.type == BotMessageType.PRODUCT and msg.product_card:
                return f"Product: {msg.product_card.title} - {msg.product_card.price} {msg.product_card.currency}"
            if msg.type == BotMessageType.CATALOG and msg.products:
                return f"Catalog: {len(msg.products)} products"
            if msg.type == BotMessageType.FORM and msg.form:
                return f"Form: {msg.form.title}"
            if msg.type == BotMessageType.RECEIPT:
                return "Payment Receipt"
            return msg.text or "Bot Message"
        if text.startswith(CB_PREFIX):
            cb = BotCallbackQuery.decode(text)
            if cb:
                return f"Selected: {cb.data}"
            return "Bot Callback"
        return text


# ── Callback Query (user → bot) ──

@dataclass
class BotCallbackQuery:
    callback_id: str
    data: str
    form_data: dict[str, str] | None = None

    def to_dict(self) -> dict[str, Any]:
        d: dict[str, Any] = {
            "callbackId": self.callback_id,
            "data": self.data,
        }
        if self.form_data is not None:
            d["formData"] = self.form_data
        return d

    def encode(self) -> str:
        return CB_PREFIX + json.dumps(self.to_dict(), separators=(",", ":"))

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> BotCallbackQuery:
        return cls(
            callback_id=data.get("callbackId", ""),
            data=data.get("data", ""),
            form_data=data.get("formData"),
        )

    @classmethod
    def decode(cls, text: str) -> BotCallbackQuery | None:
        if not text.startswith(CB_PREFIX):
            return None
        try:
            data = json.loads(text[len(CB_PREFIX):])
            return cls.from_dict(data)
        except (json.JSONDecodeError, KeyError, ValueError):
            return None


# ── Builder helpers for common message patterns ──

def text_with_buttons(
    text: str,
    keyboard: list[list[dict[str, Any] | InlineButton]],
    callback_id: str | None = None,
) -> str:
    rows = []
    for row in keyboard:
        buttons = []
        for btn in row:
            if isinstance(btn, dict):
                buttons.append(InlineButton(**btn))
            else:
                buttons.append(btn)
        rows.append(buttons)
    msg = BotMessage(
        type=BotMessageType.TEXT,
        text=text,
        keyboard=rows,
        callback_id=callback_id,
    )
    return msg.encode()


def product_card(
    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,
) -> str:
    rows = None
    if keyboard:
        rows = []
        for row in keyboard:
            buttons = []
            for btn in row:
                if isinstance(btn, dict):
                    buttons.append(InlineButton(**btn))
                else:
                    buttons.append(btn)
            rows.append(buttons)

    msg = BotMessage(
        type=BotMessageType.PRODUCT,
        product_card=ProductCard(
            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=rows,
    )
    return msg.encode()


def catalog(products: list[dict[str, Any] | ProductCard], text: str | None = None) -> str:
    cards = []
    for p in products:
        if isinstance(p, dict):
            cards.append(ProductCard(**{
                k: v for k, v in {
                    "id": p.get("id", ""),
                    "title": p.get("title", ""),
                    "price": p.get("price", "0"),
                    "currency": p.get("currency", ""),
                    "description": p.get("description"),
                    "image_url": p.get("image_url") or p.get("imageUrl"),
                    "network": p.get("network"),
                    "token_symbol": p.get("token_symbol") or p.get("tokenSymbol"),
                    "wallet_address": p.get("wallet_address") or p.get("walletAddress"),
                }.items() if v is not None
            }))
        else:
            cards.append(p)
    msg = BotMessage(type=BotMessageType.CATALOG, products=cards, text=text)
    return msg.encode()


def form(
    id: str,
    title: str,
    fields: list[dict[str, Any] | FormField],
    submit_label: str = "Submit",
    callback_data: str | None = None,
    text: str | None = None,
) -> str:
    form_fields = []
    for f in fields:
        if isinstance(f, dict):
            form_fields.append(FormField(
                name=f.get("name", ""),
                label=f.get("label", ""),
                field_type=FormFieldType(f.get("field_type", f.get("fieldType", "text"))),
                required=f.get("required", True),
                placeholder=f.get("placeholder"),
                options=f.get("options"),
            ))
        else:
            form_fields.append(f)
    msg = BotMessage(
        type=BotMessageType.FORM,
        text=text,
        form=BotForm(
            id=id,
            title=title,
            fields=form_fields,
            submit_label=submit_label,
            callback_data=callback_data,
        ),
    )
    return msg.encode()


def receipt(text: str, tx_hash: str | None = None, keyboard: list[list[dict[str, Any] | InlineButton]] | None = None) -> str:
    rows = None
    if keyboard:
        rows = []
        for row in keyboard:
            buttons = []
            for btn in row:
                if isinstance(btn, dict):
                    buttons.append(InlineButton(**btn))
                else:
                    buttons.append(btn)
            rows.append(buttons)
    msg = BotMessage(type=BotMessageType.RECEIPT, text=text, keyboard=rows)
    return msg.encode()


def media(url: str, caption: str | None = None, keyboard: list[list[dict[str, Any] | InlineButton]] | None = None) -> str:
    rows = None
    if keyboard:
        rows = []
        for row in keyboard:
            buttons = []
            for btn in row:
                if isinstance(btn, dict):
                    buttons.append(InlineButton(**btn))
                else:
                    buttons.append(btn)
            rows.append(buttons)
    msg = BotMessage(type=BotMessageType.MEDIA, media_url=url, media_caption=caption, keyboard=rows)
    return msg.encode()
