"""
BobCord SDK — fichier unique
User (selfbot) + Bot + EmbedBuilder + Builder

Python 3.9+  ·  pip install requests websocket-client

Télécharge depuis /bobcord/applications/library

Exemple bot:
    from bobcord_lib import Bot, EmbedBuilder, ActionRow, Button, WebAction

    bot = Bot("TON_TOKEN_24_CHARS")

    @bot.event
    def on_ready():
        print("connecté:", bot.me)

    @bot.command("ping")
    def ping(ctx):
        emb = (EmbedBuilder()
               .title("Pong")
               .description("ça marche")
               .color(0x5b8cff)
               .build())
        rows = [
            ActionRow(
                Button("Réagir", style="WEB_ACTION", custom_id="wa_r",
                       web_action=WebAction.react("👍")),
                Button("Popup", style="WEB_ACTION", custom_id="wa_c",
                       web_action=WebAction.confirm("Sûr ?", "popup custom",
                           then=WebAction.send_message("ok !"))),
            ).build(),
        ]
        ctx.reply("🏓", embeds=[emb], action_rows=rows)

    bot.run()

Exemple selfbot (même API, sans préfixe commandes):
    from bobcord_lib import User

    u = User("TON_TOKEN")
    u.login()

    @u.on_message
    def on_msg(ev):
        msg = ev.get("message") or {}
        if (msg.get("content") or "").strip().lower() == "!ping":
            u.send("pong", channel=msg.get("channel") or "général")

    u.run()
"""

from __future__ import annotations

import json
import threading
from typing import Any, Callable, Optional, Union

try:
    import requests
except ImportError as e:  # pragma: no cover
    raise SystemExit("pip install requests") from e

try:
    import websocket  # websocket-client
except ImportError:
    websocket = None  # type: ignore


DEFAULT_BASE = "https://aml-s9xx-box.taildd440a.ts.net/api/v1.1"
DEFAULT_WS = "wss://aml-s9xx-box.taildd440a.ts.net/api/v1.1/ws"


# ── Builder (générique) ───────────────────────────────────────────────────────

class Builder:
    """Builder fluide générique — build n'importe quel dict."""

    def __init__(self, **kwargs: Any) -> None:
        self._data: dict[str, Any] = dict(kwargs)

    def set(self, key: str, value: Any) -> "Builder":
        self._data[key] = value
        return self

    def __getattr__(self, name: str) -> Callable[[Any], "Builder"]:
        if name.startswith("_"):
            raise AttributeError(name)

        def setter(value: Any) -> "Builder":
            self._data[name] = value
            return self

        return setter

    def update(self, **kwargs: Any) -> "Builder":
        self._data.update(kwargs)
        return self

    def build(self) -> dict[str, Any]:
        return dict(self._data)

    def to_dict(self) -> dict[str, Any]:
        return self.build()

    def __repr__(self) -> str:
        return f"Builder({self._data!r})"


# ── EmbedBuilder ──────────────────────────────────────────────────────────────

class EmbedBuilder(Builder):
    """Construit un embed Discord-like (chaînage fluide)."""

    def __init__(self) -> None:
        super().__init__()

    def title(self, text: str) -> "EmbedBuilder":
        self._data["title"] = str(text)[:256]
        return self

    def description(self, text: str) -> "EmbedBuilder":
        self._data["description"] = str(text)[:4096]
        return self

    def url(self, link: str) -> "EmbedBuilder":
        self._data["url"] = str(link)[:2000]
        return self

    def color(self, value: Union[int, str]) -> "EmbedBuilder":
        if isinstance(value, str):
            value = int(value.lstrip("#"), 16)
        self._data["color"] = max(0, min(0xFFFFFF, int(value)))
        return self

    def timestamp(self, iso: Optional[str] = None) -> "EmbedBuilder":
        if iso is None:
            from datetime import datetime, timezone
            iso = datetime.now(timezone.utc).isoformat()
        self._data["timestamp"] = str(iso)[:64]
        return self

    def footer(self, text: str, *, icon_url: Optional[str] = None) -> "EmbedBuilder":
        foot: dict[str, Any] = {"text": str(text)[:2048]}
        if icon_url:
            foot["icon_url"] = str(icon_url)[:2000]
        self._data["footer"] = foot
        return self

    def image(self, url: str) -> "EmbedBuilder":
        self._data["image"] = {"url": str(url)[:2000]}
        return self

    def thumbnail(self, url: str) -> "EmbedBuilder":
        self._data["thumbnail"] = {"url": str(url)[:2000]}
        return self

    def author(
        self,
        name: str,
        *,
        url: Optional[str] = None,
        icon_url: Optional[str] = None,
    ) -> "EmbedBuilder":
        a: dict[str, Any] = {"name": str(name)[:256]}
        if url:
            a["url"] = str(url)[:2000]
        if icon_url:
            a["icon_url"] = str(icon_url)[:2000]
        self._data["author"] = a
        return self

    def field(self, name: str, value: str, *, inline: bool = False) -> "EmbedBuilder":
        fields = self._data.setdefault("fields", [])
        if len(fields) < 25:
            fields.append({
                "name": str(name)[:256] or "—",
                "value": str(value)[:1024] or "—",
                "inline": bool(inline),
            })
        return self

    def add_field(self, name: str, value: str, *, inline: bool = False) -> "EmbedBuilder":
        return self.field(name, value, inline=inline)


# ── Action rows / boutons / selects / WEB_ACTION ──────────────────────────────

def _build_comp(c: Any) -> dict:
    if hasattr(c, "build"):
        return c.build()  # type: ignore[no-any-return]
    if isinstance(c, dict):
        return c
    raise TypeError(f"composant invalide: {type(c)!r}")


class WebAction(Builder):
    """Action côté webapp (pas d'event bot). Types: confirm | send_message | react."""

    def __init__(self, **kwargs: Any) -> None:
        super().__init__(**kwargs)

    @classmethod
    def confirm(
        cls,
        title: str = "Confirmer",
        body: str = "",
        *,
        then: Optional[Union["WebAction", dict]] = None,
    ) -> "WebAction":
        wa = cls(type="confirm", title=title, body=body)
        if then is not None:
            wa.set("then", then.build() if hasattr(then, "build") else then)
        return wa

    @classmethod
    def send_message(
        cls,
        content: str,
        *,
        channel: Optional[str] = None,
        server_id: Optional[int] = None,
        confirm: Optional[dict] = None,
    ) -> "WebAction":
        wa = cls(type="send_message", content=content)
        if channel:
            wa.set("channel", channel)
        if server_id is not None:
            wa.set("server_id", server_id)
        if confirm:
            wa.set("confirm", confirm)
        return wa

    @classmethod
    def react(
        cls,
        emoji: str = "👍",
        *,
        message_id: Union[str, int] = "self",
        confirm: Optional[dict] = None,
    ) -> "WebAction":
        wa = cls(type="react", emoji=emoji, message_id=message_id)
        if confirm:
            wa.set("confirm", confirm)
        return wa


class Button(Builder):
    """Bouton action row. Styles: PRIMARY SECONDARY SUCCESS DANGER LINK WEB_ACTION."""

    def __init__(
        self,
        label: str,
        *,
        style: str = "SECONDARY",
        custom_id: Optional[str] = None,
        url: Optional[str] = None,
        web_action: Optional[Union[WebAction, dict]] = None,
        emoji: Optional[str] = None,
        disabled: bool = False,
    ) -> None:
        super().__init__(type="BUTTON", style=str(style).upper(), label=label, disabled=disabled)
        if emoji:
            self._data["emoji"] = emoji
        st = str(style).upper()
        if st == "LINK":
            if not url:
                raise ValueError("BUTTON LINK requiert url=")
            self._data["url"] = url
        else:
            if not custom_id:
                raise ValueError("BUTTON non-LINK requiert custom_id=")
            self._data["custom_id"] = custom_id
        if st == "WEB_ACTION":
            if web_action is None:
                raise ValueError("WEB_ACTION requiert web_action=")
            self._data["web_action"] = (
                web_action.build() if hasattr(web_action, "build") else web_action
            )


class StringSelect(Builder):
    def __init__(
        self,
        custom_id: str,
        options: list,
        *,
        placeholder: Optional[str] = None,
        disabled: bool = False,
    ) -> None:
        opts = []
        for o in options[:25]:
            if isinstance(o, dict):
                opts.append(
                    {
                        "label": str(o.get("label") or o.get("name") or ""),
                        "value": str(o.get("value") if o.get("value") is not None else o.get("label") or ""),
                    }
                )
            else:
                opts.append({"label": str(o), "value": str(o)})
        super().__init__(
            type="STRING_SELECT",
            custom_id=custom_id,
            options=opts,
            disabled=disabled,
        )
        if placeholder:
            self._data["placeholder"] = placeholder


class BooleanSelect(Builder):
    """Select True/False figé (non modifiable côté webapp)."""

    def __init__(self, *, value: bool = True, custom_id: str = "boolean") -> None:
        super().__init__(
            type="BOOLEAN_SELECT",
            custom_id=custom_id,
            disabled=True,
            value=bool(value),
            options=[
                {"label": "True", "value": "true", "default": bool(value)},
                {"label": "False", "value": "false", "default": not bool(value)},
            ],
        )


class UserSelect(Builder):
    def __init__(
        self,
        custom_id: str,
        *,
        placeholder: Optional[str] = None,
        disabled: bool = False,
    ) -> None:
        super().__init__(type="USER_SELECT", custom_id=custom_id, disabled=disabled)
        if placeholder:
            self._data["placeholder"] = placeholder


class ActionRow(Builder):
    """Une row (max 5 composants). Max 4 rows / message."""

    def __init__(self, *components: Any) -> None:
        comps = [_build_comp(c) for c in components[:5]]
        super().__init__(type="ACTION_ROW", components=comps)

    def add(self, *components: Any) -> "ActionRow":
        lst = list(self._data.get("components") or [])
        for c in components:
            if len(lst) >= 5:
                break
            lst.append(_build_comp(c))
        self._data["components"] = lst
        return self


def action_rows(*rows: Any) -> list:
    """Normalise une liste de ActionRow / dict (max 4)."""
    out = []
    for r in rows[:4]:
        if hasattr(r, "build"):
            out.append(r.build())
        elif isinstance(r, dict):
            out.append(r)
        elif isinstance(r, (list, tuple)):
            out.append(ActionRow(*r).build())
    return out


# ── Author (données profil dans un message) ───────────────────────────────────

class Author:
    """Profil auteur extrait d'un event message."""

    def __init__(self, data: Optional[dict] = None, **kwargs: Any) -> None:
        d = dict(data or {})
        d.update(kwargs)
        self.raw = d
        self.id = str(d.get("id") or d.get("user_id") or "")
        self.username = str(d.get("username") or d.get("name") or "")
        self.displayname = str(
            d.get("displayname") or d.get("display_name") or self.username
        )
        self.discriminator = str(d.get("discriminator") or "")
        self.avatar = d.get("avatar") or d.get("avatar_url") or d.get("avatarUrl")
        self.bot = bool(d.get("bot") or d.get("is_bot") or str(self.id).startswith("bot:"))

    @property
    def mention(self) -> str:
        return f"<@{self.id}>" if self.id else self.displayname

    @property
    def tag(self) -> str:
        if self.discriminator:
            return f"{self.username}#{self.discriminator}"
        return self.username or self.displayname

    def __repr__(self) -> str:
        return f"<Author id={self.id!r} {self.tag!r}>"

    def __str__(self) -> str:
        return self.displayname or self.username or self.id


# ── User (client selfbot) ─────────────────────────────────────────────────────

class User:
    """Client BobCord selfbot — REST + WS. Token session ou APP."""

    def __init__(
        self,
        token: str,
        *,
        base_url: str = DEFAULT_BASE,
        ws_url: str = DEFAULT_WS,
    ) -> None:
        self.token = token.strip()
        self.base_url = base_url.rstrip("/")
        self.ws_url = ws_url
        self._handlers: dict[str, list[Callable[..., Any]]] = {}
        self._ws = None
        self._ws_thread: Optional[threading.Thread] = None
        self.me: Optional[dict] = None

    def _headers(self) -> dict[str, str]:
        return {
            "Authorization": f"Bearer {self.token}",
            "Content-Type": "application/json",
        }

    def request(self, method: str, path: str, **kwargs: Any) -> Any:
        url = f"{self.base_url}/{path.lstrip('/')}"
        r = requests.request(method, url, headers=self._headers(), timeout=30, **kwargs)
        if r.status_code == 204:
            return None
        ct = r.headers.get("content-type", "")
        data = r.json() if "application/json" in ct else r.text
        if not r.ok:
            err = data.get("error") if isinstance(data, dict) else data
            raise RuntimeError(f"HTTP {r.status_code}: {err}")
        return data

    def login(self) -> dict:
        data = self.request("GET", "/me")
        self.me = (data or {}).get("user") or data
        return self.me  # type: ignore[return-value]

    def send(
        self,
        content: str = "",
        channel: str = "général",
        *,
        server_id: int = 1,
        embeds: Optional[list] = None,
        attachments: Optional[list] = None,
        action_rows: Optional[list] = None,
        components: Optional[list] = None,
    ) -> Any:
        body: dict[str, Any] = {
            "content": content,
            "channel": channel,
            "server_id": server_id,
        }
        if embeds:
            body["embeds"] = [
                e.build() if hasattr(e, "build") else e for e in embeds
            ]
        if attachments:
            body["attachments"] = attachments
        rows = action_rows if action_rows is not None else components
        if rows:
            body["action_rows"] = [
                r.build() if hasattr(r, "build") else r for r in rows
            ]
        return self.request("POST", "/message", json=body)

    def edit(self, message_id: int | str, content: str) -> Any:
        return self.request("PUT", f"/message?id={message_id}", json={"content": content})

    def delete(self, message_id: int | str) -> Any:
        return self.request("DELETE", "/message", json={"id": message_id})

    def react(self, message_id: int | str, emoji: str) -> Any:
        return self.request("PUT", "/message/react", json={"id": message_id, "emoji": emoji})

    def get_message(self, message_id: int | str) -> dict:
        return self.request("GET", f"/message?id={message_id}")

    def get_user(self, user_id: str) -> dict:
        return self.request("GET", f"/user?id={user_id}")

    def on(self, event: str, handler: Callable[..., Any]) -> Callable:
        self._handlers.setdefault(event, []).append(handler)
        return handler

    def on_message(self, handler: Callable[..., Any]) -> Callable:
        return self.on("message", handler)

    def _emit(self, event: str, payload: Any = None) -> None:
        for h in self._handlers.get(event, []):
            try:
                if payload is None:
                    h()
                else:
                    h(payload)
            except TypeError:
                try:
                    h()
                except Exception as e:  # noqa: BLE001
                    print(f"[User] handler {event} error:", e)
            except Exception as e:  # noqa: BLE001
                print(f"[User] handler {event} error:", e)

    def run(self, *, blocking: bool = True) -> None:
        """Connecte le WS (réception live). Envoi = REST uniquement."""
        if websocket is None:
            raise SystemExit("pip install websocket-client")

        def _on_message(_ws, raw: str) -> None:
            try:
                data = json.loads(raw)
            except json.JSONDecodeError:
                return
            et = data.get("type") or "raw"
            self._emit(et, data)
            if et == "message":
                self._emit("message", data)

        def _on_open(_ws) -> None:
            self._emit("ready", {"ok": True, "me": self.me})

        url = f"{self.ws_url}?token={self.token}"
        self._ws = websocket.WebSocketApp(
            url,
            on_message=_on_message,
            on_open=_on_open,
        )

        def _run() -> None:
            assert self._ws is not None
            self._ws.run_forever(ping_interval=25, ping_timeout=10)

        if blocking:
            _run()
        else:
            self._ws_thread = threading.Thread(target=_run, daemon=True)
            self._ws_thread.start()


# ── Context (commande Bot) ────────────────────────────────────────────────────

class Context:
    """Contexte passé aux handlers @bot.command."""

    def __init__(
        self,
        bot: "Bot",
        message: dict,
        *,
        server_id: int = 1,
        args: str = "",
        event: Optional[dict] = None,
    ) -> None:
        self.bot = bot
        self.message = message
        self.event = event or {}
        self.server_id = int(server_id)
        self.channel = message.get("channel") or "général"
        self.content = str(message.get("content") or "")
        self.args = args
        author = message.get("author") or {}
        self.author = Author(author) if isinstance(author, dict) else Author()
        self.message_id = message.get("id")

    def reply(
        self,
        content: str = "",
        *,
        embeds: Optional[list] = None,
        attachments: Optional[list] = None,
        action_rows: Optional[list] = None,
        components: Optional[list] = None,
    ) -> Any:
        return self.bot.send(
            content,
            channel=self.channel,
            server_id=self.server_id,
            embeds=embeds,
            attachments=attachments,
            action_rows=action_rows if action_rows is not None else components,
        )

    def react(self, emoji: str) -> Any:
        if self.message_id is None:
            raise RuntimeError("pas d'id message")
        return self.bot.react(self.message_id, emoji)


# ── Bot (client APP + décorateurs) ────────────────────────────────────────────

class Bot(User):
    """Client bot APP — héritage de User + @event / @command."""

    def __init__(
        self,
        token: str,
        *,
        base_url: str = DEFAULT_BASE,
        ws_url: str = DEFAULT_WS,
        prefix: str = "!",
        intents: Optional[list] = None,  # réservé
    ) -> None:
        super().__init__(token, base_url=base_url, ws_url=ws_url)
        self.prefix = prefix
        self._commands: dict[str, Callable[[Context], Any]] = {}
        self.user: Optional[Author] = None  # profil après login

    def login(self) -> dict:
        me = super().login()
        self.user = Author(me if isinstance(me, dict) else {})
        return me

    def event(self, name: Optional[str] = None) -> Callable:
        """
        @bot.event
        def on_ready(): ...

        @bot.event("message")
        def whatever(ev): ...
        """
        def deco(fn: Callable) -> Callable:
            ev = name or fn.__name__
            key = ev[3:] if ev.startswith("on_") else ev
            self._handlers.setdefault(key, []).append(fn)
            self._handlers.setdefault(ev, []).append(fn)
            return fn

        if callable(name):
            fn = name
            name = None
            return deco(fn)
        return deco

    def command(self, name: Optional[str] = None, *, aliases: Optional[list[str]] = None) -> Callable:
        """
        @bot.command
        def ping(ctx): ...

        @bot.command("hello")
        def hi(ctx): ...
        """
        def deco(fn: Callable) -> Callable:
            cmd = (name or fn.__name__).lower().lstrip(self.prefix)
            self._commands[cmd] = fn
            for a in aliases or []:
                self._commands[str(a).lower().lstrip(self.prefix)] = fn
            return fn

        if callable(name):
            fn = name
            name = None
            return deco(fn)
        return deco

    def _emit(self, event: str, payload: Any = None) -> None:
        seen: set[int] = set()
        for key in (event, f"on_{event}"):
            for h in self._handlers.get(key, []):
                if id(h) in seen:
                    continue
                seen.add(id(h))
                try:
                    if payload is None:
                        h()
                    else:
                        h(payload)
                except TypeError:
                    try:
                        h()
                    except Exception as e:  # noqa: BLE001
                        print(f"[Bot] handler {key} error:", e)
                except Exception as e:  # noqa: BLE001
                    print(f"[Bot] handler {key} error:", e)

    def _handle_message_event(self, data: dict) -> None:
        msg = data.get("message") or {}
        if not isinstance(msg, dict):
            return
        sid = int(data.get("server_id") or msg.get("server_id") or 1)
        content = str(msg.get("content") or "")
        self._emit("message", data)

        text = content.strip()
        if not text.startswith(self.prefix):
            return
        body = text[len(self.prefix):]
        parts = body.split(None, 1)
        if not parts:
            return
        cmd = parts[0].lower()
        args = parts[1] if len(parts) > 1 else ""
        handler = self._commands.get(cmd)
        if not handler:
            return
        ctx = Context(self, msg, server_id=sid, args=args, event=data)
        try:
            handler(ctx)
        except Exception as e:  # noqa: BLE001
            print(f"[Bot] command !{cmd} error:", e)

    def run(self, *, blocking: bool = True) -> None:
        if websocket is None:
            raise SystemExit("pip install websocket-client")

        try:
            self.login()
        except Exception as e:  # noqa: BLE001
            print("[Bot] login warning:", e)

        def _on_message(_ws, raw: str) -> None:
            try:
                data = json.loads(raw)
            except json.JSONDecodeError:
                return
            et = data.get("type") or "raw"
            if et == "message":
                self._handle_message_event(data)
            else:
                self._emit(et, data)

        def _on_open(_ws) -> None:
            self._emit("ready")

        url = f"{self.ws_url}?token={self.token}"
        self._ws = websocket.WebSocketApp(
            url,
            on_message=_on_message,
            on_open=_on_open,
        )

        def _run() -> None:
            assert self._ws is not None
            self._ws.run_forever(ping_interval=25, ping_timeout=10)

        if blocking:
            _run()
        else:
            self._ws_thread = threading.Thread(target=_run, daemon=True)
            self._ws_thread.start()


# Alias
Client = Bot
Selfbot = User


# ── Exemple ───────────────────────────────────────────────────────────────────
if __name__ == "__main__":
    TOKEN = "TON_TOKEN_24_CHARS"  # Applications → Voir token

    bot = Bot(TOKEN)

    @bot.event
    def on_ready() -> None:
        print("prêt:", bot.user)

    @bot.command("ping")
    def ping(ctx: Context) -> None:
        emb = (
            EmbedBuilder()
            .title("Pong")
            .description(f"salut {ctx.author.displayname}")
            .color(0x5B8CFF)
            .footer("BobCord SDK")
            .build()
        )
        ctx.reply(embeds=[emb])

    @bot.command("user")
    def userinfo(ctx: Context) -> None:
        u = ctx.author
        emb = (
            EmbedBuilder()
            .title(str(u))
            .field("id", u.id, inline=True)
            .field("tag", u.tag, inline=True)
            .field("bot", str(u.bot), inline=True)
            .color(0x3DD68C)
            .build()
        )
        ctx.reply(embeds=[emb])

    print("écoute WS… Ctrl+C pour stop")
    bot.run()
