#!/usr/bin/env python3
from __future__ import annotations

import json
import sys
import threading
import time
from typing import Any, Dict, List, Optional, Tuple

import requests

try:  # pragma: no cover
    import websocket as websocket_client
except Exception:  # pragma: no cover
    websocket_client = None

from cheapclaw_channel_base import ChannelAdapter
from tool_runtime_helpers import now_iso


class DiscordAdapter(ChannelAdapter):
    name = "discord"
    DEFAULT_INTENTS = 1 + 512 + 4096 + 32768

    def __init__(self, config: Dict[str, Any], service: Any, *, logger=None):
        super().__init__(config, service, logger=logger)
        self.bot_token = str(config.get("bot_token") or "").strip()
        self.bot_id = str(config.get("bot_id") or service.bot_id).strip() or service.bot_id
        self.display_name = str(config.get("display_name") or service.bot_display_name or self.bot_id).strip() or self.bot_id
        self.require_mention_in_guild = bool(config.get("require_mention_in_guild", True))
        self.intents = int(config.get("intents") or self.DEFAULT_INTENTS)
        self.api_root = "https://discord.com/api/v10"
        self._queue_lock = threading.Lock()
        self._queued_events: List[Dict[str, Any]] = []
        self._gateway_thread: Optional[threading.Thread] = None
        self._gateway_started = False
        self._stop_event = threading.Event()
        self._seq: Optional[int] = None
        self._bot_user_id = str(config.get("bot_user_id") or "").strip()
        if self.bot_token:
            self._start_gateway()

    def _enqueue_event(self, payload: Dict[str, Any]) -> None:
        with self._queue_lock:
            self._queued_events.append(payload)

    def poll_events(self) -> List[Dict[str, Any]]:
        with self._queue_lock:
            items = list(self._queued_events)
            self._queued_events.clear()
            return items

    def _auth_headers(self) -> Dict[str, str]:
        return {"Authorization": f"Bot {self.bot_token}", "Content-Type": "application/json"}

    def _get_gateway_url(self) -> str:
        if not self.bot_token:
            return ""
        try:
            response = requests.get(f"{self.api_root}/gateway/bot", headers=self._auth_headers(), timeout=30)
            response.raise_for_status()
            payload = response.json() if response.content else {}
            return str(payload.get("url") or "").strip()
        except Exception as exc:
            self.log(f"Discord gateway url fetch failed: {exc}")
            return ""

    def _identify_gateway(self, ws: Any) -> None:
        payload = {
            "op": 2,
            "d": {
                "token": self.bot_token,
                "intents": self.intents,
                "properties": {"os": sys.platform, "browser": "cheapclaw", "device": "cheapclaw"},
            },
        }
        ws.send(json.dumps(payload, ensure_ascii=False))

    def _normalize_gateway_message(self, message: Dict[str, Any]) -> Optional[Dict[str, Any]]:
        if not isinstance(message, dict):
            return None
        author = message.get("author") or {}
        if bool(author.get("bot")):
            return None
        channel_id = str(message.get("channel_id") or "").strip()
        if not channel_id:
            return None
        guild_id = str(message.get("guild_id") or "").strip()
        content = str(message.get("content") or "")
        mentions_payload = message.get("mentions") or []
        mentioned_ids: List[str] = []
        for item in mentions_payload:
            if not isinstance(item, dict):
                continue
            user_id = str(item.get("id") or "").strip()
            if user_id and user_id not in mentioned_ids:
                mentioned_ids.append(user_id)
        mention_to_bot = True
        require_mention = False
        if guild_id:
            require_mention = self.require_mention_in_guild
            mention_to_bot = False
            if self._bot_user_id and self._bot_user_id in mentioned_ids:
                mention_to_bot = True
            if self._bot_user_id and (f"<@{self._bot_user_id}>" in content or f"<@!{self._bot_user_id}>" in content):
                mention_to_bot = True
        attachment_items: List[Dict[str, Any]] = []
        for item in (message.get("attachments") or []):
            if not isinstance(item, dict):
                continue
            url = str(item.get("url") or "").strip()
            if not url:
                continue
            attachment_items.append(
                {
                    "kind": "url",
                    "url": url,
                    "filename": str(item.get("filename") or "").strip(),
                    "mime_type": str(item.get("content_type") or "").strip(),
                }
            )
        sender_id = str(author.get("id") or "").strip()
        sender_name = str(author.get("global_name") or author.get("username") or sender_id).strip() or sender_id
        conversation_id = f"guild:{guild_id}:channel:{channel_id}" if guild_id else f"dm:{channel_id}"
        conversation_type = "group" if guild_id else "person"
        timestamp_raw = str(message.get("timestamp") or "").strip()
        return {
            "event_id": f"discord_{message.get('id')}",
            "channel": "discord",
            "conversation_id": conversation_id,
            "conversation_type": conversation_type,
            "display_name": conversation_id,
            "sender_id": sender_id,
            "sender_name": sender_name,
            "message_id": str(message.get("id") or ""),
            "message_text": content,
            "attachments": attachment_items,
            "timestamp": timestamp_raw or now_iso(),
            "is_mention_to_bot": bool(mention_to_bot),
            "require_mention": bool(require_mention),
        }

    def _start_gateway(self) -> None:
        if self._gateway_started:
            return
        self._gateway_started = True
        if websocket_client is None:
            self.log("Discord gateway skipped: websocket-client is not installed")
            return

        def _runner() -> None:
            while not self._stop_event.is_set():
                gateway_url = self._get_gateway_url()
                if not gateway_url:
                    time.sleep(5)
                    continue
                ws = None
                try:
                    ws = websocket_client.create_connection(
                        f"{gateway_url}?v=10&encoding=json",
                        timeout=30,
                        enable_multithread=True,
                    )
                    ws.settimeout(1.0)
                    heartbeat_interval = 45.0
                    next_heartbeat = time.time() + heartbeat_interval
                    identified = False
                    while not self._stop_event.is_set():
                        if identified and time.time() >= next_heartbeat:
                            ws.send(json.dumps({"op": 1, "d": self._seq}))
                            next_heartbeat = time.time() + heartbeat_interval
                        try:
                            raw = ws.recv()
                        except websocket_client.WebSocketTimeoutException:
                            continue
                        if not raw:
                            continue
                        packet = json.loads(raw)
                        if not isinstance(packet, dict):
                            continue
                        op = int(packet.get("op") or 0)
                        seq = packet.get("s")
                        event_type = str(packet.get("t") or "")
                        data = packet.get("d") if isinstance(packet.get("d"), dict) else packet.get("d")
                        if isinstance(seq, int):
                            self._seq = seq
                        if op == 10 and isinstance(data, dict):
                            heartbeat_interval = max(5.0, float(data.get("heartbeat_interval") or 45000) / 1000.0)
                            next_heartbeat = time.time() + heartbeat_interval
                            self._identify_gateway(ws)
                            identified = True
                            continue
                        if op == 11:
                            continue
                        if op in {7, 9}:
                            break
                        if op == 0 and event_type == "READY" and isinstance(data, dict):
                            user = data.get("user") or {}
                            if isinstance(user, dict):
                                self._bot_user_id = str(user.get("id") or self._bot_user_id).strip() or self._bot_user_id
                            continue
                        if op == 0 and event_type == "MESSAGE_CREATE" and isinstance(data, dict):
                            normalized = self._normalize_gateway_message(data)
                            if normalized:
                                self._enqueue_event(normalized)
                except Exception as exc:
                    self.log(f"Discord gateway loop error: {exc}")
                finally:
                    if ws is not None:
                        try:
                            ws.close()
                        except Exception:
                            pass
                time.sleep(3)

        self._gateway_thread = threading.Thread(target=_runner, daemon=True, name="cheapclaw-discord-gateway")
        self._gateway_thread.start()

    @staticmethod
    def _channel_id_from_conversation(conversation_id: str) -> str:
        raw = str(conversation_id or "").strip()
        if ":channel:" in raw:
            return raw.split(":channel:", 1)[1].strip()
        if raw.startswith("dm:"):
            return raw[3:].strip()
        return raw

    def send_message(self, conversation_id: str, message: str, attachments: Optional[List[Dict[str, Any]]] = None) -> Tuple[bool, str]:
        if not self.bot_token:
            return False, "discord bot_token is missing"
        channel_id = self._channel_id_from_conversation(conversation_id)
        if not channel_id:
            return False, "discord conversation_id is missing channel id"
        normalized = self._normalize_attachments(attachments)
        if not message and not normalized:
            return False, "discord message or attachments are required"

        url = f"{self.api_root}/channels/{channel_id}/messages"
        headers = {"Authorization": f"Bot {self.bot_token}"}
        try:
            if normalized:
                file_handles = []
                files = []
                attachment_meta: List[Dict[str, Any]] = []
                for idx, item in enumerate(normalized):
                    fh = open(item["path"], "rb")
                    file_handles.append(fh)
                    files.append((f"files[{idx}]", (item["filename"], fh, item["mime_type"])))
                    attachment_meta.append({"id": idx, "filename": item["filename"], "description": item["caption"] or ""})
                payload = {"content": message or "", "attachments": attachment_meta}
                data = {"payload_json": json.dumps(payload, ensure_ascii=False)}
                response = requests.post(url, headers=headers, data=data, files=files, timeout=60)
                for fh in file_handles:
                    fh.close()
            else:
                response = requests.post(url, headers={**headers, "Content-Type": "application/json"}, json={"content": message}, timeout=30)
            if response.status_code >= 300:
                return False, response.text
            payload = response.json() if response.content else {}
            return True, str(payload.get("id") or "")
        except Exception as exc:
            return False, str(exc)
