"""SalmAlm Discord Bot — Pure stdlib Discord Gateway + HTTP API."""

from __future__ import annotations

import asyncio
import json
import os
import struct
import ssl
import urllib.request
import urllib.parse
from typing import Optional, Dict, Any, Callable

from salmalm import log

API_BASE = "https://discord.com/api/v10"


class DiscordBot:
    """Minimal Discord bot using Gateway WebSocket + REST API."""

    def __init__(self):
        self.token: Optional[str] = None
        self.owner_id: Optional[str] = None
        self._running = False
        self._heartbeat_interval = 41250
        self._seq: Optional[int] = None
        self._session_id: Optional[str] = None
        self._bot_user: Optional[Dict] = None
        self._on_message: Optional[Callable] = None
        self._ws = None

    def configure(self, token: str, owner_id: Optional[str] = None):
        """Configure the Discord bot with token and channel settings."""
        self.token = token
        self.owner_id = owner_id

    def on_message(self, func):
        """Decorator to register message handler."""
        self._on_message = func
        return func

    # ── REST API ──

    def _api(self, method: str, path: str, body: Optional[dict] = None) -> dict:
        url = f"{API_BASE}{path}"
        data = json.dumps(body).encode() if body else None
        req = urllib.request.Request(url, data=data, method=method)
        req.add_header("Authorization", f"Bot {self.token}")
        req.add_header("Content-Type", "application/json")
        req.add_header("User-Agent", "SalmAlm (github.com/hyunjun6928-netizen/salmalm, 0.8)")
        try:
            with urllib.request.urlopen(req, timeout=15) as resp:
                return json.loads(resp.read().decode())  # type: ignore[no-any-return]
        except urllib.error.HTTPError as e:
            err = e.read().decode("utf-8", errors="replace")
            log.error(f"Discord API {method} {path}: {e.code} {err[:200]}")
            return {}

    def send_message(self, channel_id: str, content: str, reply_to: Optional[str] = None) -> dict:
        """Send a message to a channel."""
        body: Dict[str, Any] = {"content": content[:2000]}
        if reply_to:
            body["message_reference"] = {"message_id": reply_to}
        return self._api("POST", f"/channels/{channel_id}/messages", body)

    def send_typing(self, channel_id: str):
        """Send typing indicator."""
        self._api("POST", f"/channels/{channel_id}/typing")

    def start_typing_loop(self, channel_id: str) -> "asyncio.Task":
        """Start a continuous typing indicator loop (refreshes every 8s).

        Discord typing expires after ~10s, so we refresh every 8s.
        Returns a cancellable asyncio Task.
        """

        async def _loop():
            try:
                while True:
                    await asyncio.to_thread(self.send_typing, channel_id)
                    await asyncio.sleep(8)
            except asyncio.CancelledError:
                pass

        return asyncio.create_task(_loop())

    def add_reaction(self, channel_id: str, message_id: str, emoji: str):
        """Add an emoji reaction to a Discord message."""
        encoded = urllib.parse.quote(emoji)
        self._api("PUT", f"/channels/{channel_id}/messages/{message_id}/reactions/{encoded}/@me")

    # ── Gateway WebSocket ──

    async def _gateway_connect(self):
        """Connect to Discord Gateway via raw SSL socket."""
        import socket

        # Get gateway URL
        gw = self._api("GET", "/gateway/bot")
        gateway_url = gw.get("url", "wss://gateway.discord.gg")
        gateway_url += "/?v=10&encoding=json"

        # Parse URL
        host = gateway_url.split("//")[1].split("/")[0].split("?")[0]
        path = "/" + "/".join(gateway_url.split("//")[1].split("/")[1:])

        # SSL connect
        ctx = ssl.create_default_context()
        sock = socket.create_connection((host, 443), timeout=30)
        self._ws_raw = ctx.wrap_socket(sock, server_hostname=host)

        # WebSocket handshake
        key = __import__("base64").b64encode(os.urandom(16)).decode()
        handshake = (
            f"GET {path} HTTP/1.1\r\n"
            f"Host: {host}\r\n"
            f"Upgrade: websocket\r\n"
            f"Connection: Upgrade\r\n"
            f"Sec-WebSocket-Key: {key}\r\n"
            f"Sec-WebSocket-Version: 13\r\n"
            f"\r\n"
        )
        self._ws_raw.sendall(handshake.encode())

        # Read handshake response
        resp = b""
        while b"\r\n\r\n" not in resp:
            resp += self._ws_raw.recv(4096)

        if b"101" not in resp.split(b"\r\n")[0]:
            raise ConnectionError(f"WebSocket handshake failed: {resp[:100]}")  # type: ignore[str-bytes-safe]

        log.info("[DISC] Discord Gateway connected")

    def _ws_send(self, data: dict):
        """Send a WebSocket frame (masked, as client)."""
        payload = json.dumps(data).encode()
        # Build frame: FIN + TEXT opcode
        frame = bytearray([0x81])
        length = len(payload)
        if length < 126:
            frame.append(0x80 | length)  # masked
        elif length < 65536:
            frame.append(0x80 | 126)
            frame.extend(struct.pack(">H", length))
        else:
            frame.append(0x80 | 127)
            frame.extend(struct.pack(">Q", length))
        # Mask
        mask = os.urandom(4)
        frame.extend(mask)
        frame.extend(bytes(b ^ mask[i % 4] for i, b in enumerate(payload)))
        self._ws_raw.sendall(frame)

    def _ws_recv(self) -> Optional[dict]:
        """Receive a WebSocket frame."""
        try:
            header = self._ws_raw.recv(2)
            if len(header) < 2:
                return None
            opcode = header[0] & 0x0F
            masked = header[1] & 0x80
            length = header[1] & 0x7F

            if length == 126:
                length = struct.unpack(">H", self._ws_raw.recv(2))[0]
            elif length == 127:
                length = struct.unpack(">Q", self._ws_raw.recv(8))[0]

            if masked:
                mask = self._ws_raw.recv(4)

            data = b""
            while len(data) < length:
                chunk = self._ws_raw.recv(min(length - len(data), 65536))
                if not chunk:
                    return None
                data += chunk

            if masked:
                data = bytes(b ^ mask[i % 4] for i, b in enumerate(data))

            if opcode == 0x08:  # Close
                return None
            if opcode == 0x09:  # Ping
                # Send pong
                pong = bytearray([0x8A, 0x80 | len(data)])
                mask = os.urandom(4)
                pong.extend(mask)
                pong.extend(bytes(b ^ mask[i % 4] for i, b in enumerate(data)))
                self._ws_raw.sendall(pong)
                return self._ws_recv()
            if opcode == 0x01:  # Text
                return json.loads(data.decode())  # type: ignore[no-any-return]
            return None
        except Exception as e:
            log.error(f"Discord WS recv error: {e}")
            return None

    async def _heartbeat_loop(self):
        """Send heartbeat at the required interval."""
        while self._running:
            await asyncio.sleep(self._heartbeat_interval / 1000)
            if self._running:
                self._ws_send({"op": 1, "d": self._seq})

    async def _identify(self):
        """Send IDENTIFY payload."""
        self._ws_send(
            {
                "op": 2,
                "d": {
                    "token": self.token,
                    "intents": 33281,  # GUILDS + GUILD_MESSAGES + DM_MESSAGES + MESSAGE_CONTENT
                    "properties": {"os": "linux", "browser": "salmalm", "device": "salmalm"},
                },
            }
        )

    async def _handle_event(self, data: dict):
        """Handle a gateway event."""
        op = data.get("op")
        t = data.get("t")
        d = data.get("d", {})
        s = data.get("s")

        if s:
            self._seq = s

        if op == 10:  # Hello
            self._heartbeat_interval = d.get("heartbeat_interval", 41250)
            asyncio.create_task(self._heartbeat_loop())
            await self._identify()

        elif op == 11:  # Heartbeat ACK
            pass

        elif op == 0:  # Dispatch
            if t == "READY":
                self._session_id = d.get("session_id")
                self._bot_user = d.get("user", {})
                log.info(
                    f"[DISC] Discord ready: {self._bot_user.get('username')}#{self._bot_user.get('discriminator')}"
                )

            elif t == "MESSAGE_CREATE":
                # Ignore own messages
                author = d.get("author", {})
                if author.get("id") == self._bot_user.get("id"):  # type: ignore[union-attr]
                    return
                if author.get("bot"):
                    return

                content = d.get("content", "").strip()
                channel_id = d.get("channel_id")
                message_id = d.get("id")

                # Check if bot is mentioned or DM
                is_dm = d.get("guild_id") is None
                mentions = [m.get("id") for m in d.get("mentions", [])]
                is_mentioned = self._bot_user and self._bot_user.get("id") in mentions

                if not is_dm and not is_mentioned:
                    return  # Only respond to DMs and mentions

                # Strip bot mention from content
                if self._bot_user:
                    content = content.replace(f"<@{self._bot_user['id']}>", "").strip()
                    content = content.replace(f"<@!{self._bot_user['id']}>", "").strip()

                if not content:
                    return

                if self._on_message:
                    # Start continuous typing indicator
                    typing_task = self.start_typing_loop(channel_id)
                    try:
                        response = await self._on_message(content, d)
                        if response:
                            # Split long messages
                            while response:
                                chunk = response[:2000]
                                response = response[2000:]
                                self.send_message(channel_id, chunk, reply_to=message_id)
                    except Exception as e:
                        log.error(f"Discord message handler error: {e}")
                        self.send_message(channel_id, f"❌ Error: {str(e)[:200]}", reply_to=message_id)
                    finally:
                        typing_task.cancel()
                        try:
                            await typing_task
                        except asyncio.CancelledError:
                            pass

    async def poll(self):
        """Main gateway loop."""
        if not self.token:
            log.warning("Discord token not configured")
            return

        self._running = True
        retry_delay = 1

        while self._running:
            try:
                await self._gateway_connect()
                retry_delay = 1

                while self._running:
                    data = await asyncio.to_thread(self._ws_recv)
                    if data is None:
                        break
                    await self._handle_event(data)

            except Exception as e:
                log.error(f"Discord gateway error: {e}")

            if self._running:
                log.info(f"[DISC] Discord reconnecting in {retry_delay}s...")
                await asyncio.sleep(retry_delay)
                retry_delay = min(retry_delay * 2, 60)

    def stop(self):
        """Stop the Discord bot."""
        self._running = False
        try:
            if self._ws_raw:
                self._ws_raw.close()
        except Exception as e:
            log.debug(f"Suppressed: {e}")


discord_bot = DiscordBot()
