import asyncio
import re
import sys
from typing import Any, cast

import discord
from discord.abc import GuildChannel, Messageable, PrivateChannel
from discord.channel import DMChannel

from astrbot import logger
from astrbot.api.event import MessageChain
from astrbot.api.message_components import File, Image, Plain, Record
from astrbot.api.platform import (
    AstrBotMessage,
    MessageMember,
    MessageType,
    Platform,
    PlatformMetadata,
    register_platform_adapter,
)
from astrbot.core.platform.astr_message_event import MessageSesion
from astrbot.core.star.filter.command import CommandFilter
from astrbot.core.star.filter.command_group import CommandGroupFilter
from astrbot.core.star.star import star_map
from astrbot.core.star.star_handler import StarHandlerMetadata, star_handlers_registry
from astrbot.core.utils.media_utils import MediaResolver

from .client import DiscordBotClient
from .discord_platform_event import DiscordPlatformEvent

if sys.version_info >= (3, 12):
    from typing import override
else:
    from typing_extensions import override


# 注册平台适配器
@register_platform_adapter(
    "discord", "Discord 适配器 (基于 Pycord)", support_streaming_message=False
)
class DiscordPlatformAdapter(Platform):
    def __init__(
        self,
        platform_config: dict,
        platform_settings: dict,
        event_queue: asyncio.Queue,
    ) -> None:
        super().__init__(platform_config, event_queue)
        self.settings = platform_settings
        self.bot_self_id: str | None = None
        self.registered_handlers = []
        # 指令注册相关
        self.enable_command_register = self.config.get("discord_command_register", True)
        self.guild_id = self.config.get("discord_guild_id_for_debug", None)
        self.activity_name = self.config.get("discord_activity_name", None)
        self.shutdown_event = asyncio.Event()
        self._polling_task = None

    @override
    async def send_by_session(
        self,
        session: MessageSesion,
        message_chain: MessageChain,
    ) -> None:
        """通过会话发送消息"""
        if self.client.user is None:
            logger.error(
                "[Discord] Client is not ready (self.client.user is None); message send skipped"
            )
            return

        # 创建一个 message_obj 以便在 event 中使用
        message_obj = AstrBotMessage()
        if "_" in session.session_id:
            session.session_id = session.session_id.split("_")[1]
        channel_id_str = session.session_id
        channel = None
        try:
            channel_id = int(channel_id_str)
            channel = self.client.get_channel(channel_id)
        except (ValueError, TypeError):
            logger.warning(f"[Discord] Invalid channel ID format: {channel_id_str}")

        if channel:
            message_obj.type = self._get_message_type(channel)
            message_obj.group_id = self._get_channel_id(channel)
        else:
            logger.warning(
                f"[Discord] Can't get channel info for {channel_id_str}, will guess message type.",
            )
            message_obj.type = MessageType.GROUP_MESSAGE
            message_obj.group_id = session.session_id

        message_obj.message_str = message_chain.get_plain_text()
        message_obj.sender = MessageMember(
            user_id=str(self.bot_self_id),
            nickname=self.client.user.display_name,
        )
        message_obj.self_id = cast(str, self.bot_self_id)
        message_obj.session_id = session.session_id
        message_obj.message = message_chain.chain

        temp_event = self.create_event(message_obj)
        await temp_event.send(message_chain)
        await super().send_by_session(session, message_chain)

    @override
    def meta(self) -> PlatformMetadata:
        """返回平台元数据"""
        return PlatformMetadata(
            "discord",
            "Discord Adapter",
            id=cast(str, self.config.get("id")),
            default_config_tmpl=self.config,
            support_streaming_message=False,
        )

    @override
    async def run(self) -> None:
        """主要运行逻辑"""

        # 初始化回调函数
        async def on_received(message_data) -> None:
            logger.debug(f"[Discord] Message received: {message_data}")
            if self.bot_self_id is None:
                self.bot_self_id = message_data.get("bot_id")
            abm = await self.convert_message(data=message_data)
            await self.handle_msg(abm)

        # 初始化 Discord 客户端
        token = str(self.config.get("discord_token"))
        if not token:
            logger.error(
                "[Discord] Bot token is not configured. Please set a valid token in the config file."
            )
            return

        proxy = self.config.get("discord_proxy") or None
        allow_bot_messages = bool(self.config.get("discord_allow_bot_messages"))
        self.client = DiscordBotClient(token, proxy, allow_bot_messages)
        self.client.on_message_received = on_received

        async def callback() -> None:
            try:
                if self.enable_command_register:
                    await self._collect_and_register_commands()
                if self.activity_name:
                    await self.client.change_presence(
                        status=discord.Status.online,
                        activity=discord.CustomActivity(name=self.activity_name),
                    )
            except Exception as e:
                logger.error(
                    f"[Discord] on_ready_once_callback err: {e}", exc_info=True
                )

        self.client.on_ready_once_callback = callback

        try:
            self._polling_task = asyncio.create_task(self.client.start_polling())
            await self.shutdown_event.wait()
        except discord.errors.LoginFailure:
            logger.error(
                "[Discord] Login failed. Please check whether the bot token is correct."
            )
        except discord.errors.ConnectionClosed:
            logger.warning("[Discord] Connection with Discord has been closed.")
        except Exception as e:
            logger.error(
                f"[Discord] Unexpected error while adapter is running: {e}",
                exc_info=True,
            )

    def _get_message_type(
        self,
        channel: Messageable | GuildChannel | PrivateChannel,
        guild_id: int | None = None,
    ) -> MessageType:
        """根据 channel 对象和 guild_id 判断消息类型"""
        if guild_id is not None:
            return MessageType.GROUP_MESSAGE
        if isinstance(channel, DMChannel) or getattr(channel, "guild", None) is None:
            return MessageType.FRIEND_MESSAGE
        return MessageType.GROUP_MESSAGE

    def _get_channel_id(
        self, channel: Messageable | GuildChannel | PrivateChannel
    ) -> str:
        """根据 channel 对象获取ID"""
        return str(getattr(channel, "id", None))

    def _convert_message_to_abm(self, data: dict) -> AstrBotMessage:
        """将普通消息转换为 AstrBotMessage"""
        message = data["message"]

        content = message.content

        # 如果机器人被@，移除@部分
        # 剥离 User Mention (<@id>, <@!id>)
        if self.client and self.client.user:
            mention_str = f"<@{self.client.user.id}>"
            mention_str_nickname = f"<@!{self.client.user.id}>"
            if content.startswith(mention_str):
                content = content[len(mention_str) :].lstrip()
            elif content.startswith(mention_str_nickname):
                content = content[len(mention_str_nickname) :].lstrip()

        # 剥离 Role Mention（bot 拥有的任一角色被提及，<@&role_id>）
        if (
            hasattr(message, "role_mentions")
            and hasattr(message, "guild")
            and message.guild
        ):
            bot_member = (
                message.guild.get_member(self.client.user.id)
                if self.client and self.client.user
                else None
            )
            if bot_member and hasattr(bot_member, "roles"):
                for role in bot_member.roles:
                    role_mention_str = f"<@&{role.id}>"
                    if content.startswith(role_mention_str):
                        content = content[len(role_mention_str) :].lstrip()
                        break  # 只剥离第一个匹配的角色 mention

        abm = AstrBotMessage()
        abm.type = self._get_message_type(message.channel)
        abm.group_id = self._get_channel_id(message.channel)
        abm.message_str = content
        abm.sender = MessageMember(
            user_id=str(message.author.id),
            nickname=message.author.display_name,
        )
        message_chain = []
        if abm.message_str:
            message_chain.append(Plain(text=abm.message_str))
        if message.attachments:
            for attachment in message.attachments:
                if attachment.content_type and attachment.content_type.startswith(
                    "image/",
                ):
                    message_chain.append(
                        Image(file=attachment.url, filename=attachment.filename),
                    )
                elif attachment.content_type and attachment.content_type.startswith(
                    "audio/",
                ):
                    message_chain.append(
                        Record(file=attachment.url, url=attachment.url),
                    )
                else:
                    message_chain.append(
                        File(name=attachment.filename, url=attachment.url),
                    )
        abm.message = message_chain
        abm.raw_message = message
        abm.self_id = cast(str, self.bot_self_id)
        abm.session_id = str(message.channel.id)
        abm.message_id = str(message.id)
        return abm

    async def convert_message(self, data: dict) -> AstrBotMessage:
        """将平台消息转换成 AstrBotMessage"""
        # 由于 on_interaction 已被禁用，我们只处理普通消息
        abm = self._convert_message_to_abm(data)
        for component in abm.message:
            if isinstance(component, Record):
                audio_ref = component.url or component.file
                if audio_ref:
                    path_wav = await MediaResolver(
                        audio_ref,
                        media_type="audio",
                        default_suffix=".wav",
                    ).to_path(target_format="wav")
                    component.file = path_wav
                    component.url = path_wav
                    component.path = path_wav
        return abm

    def create_event(
        self, message: AstrBotMessage, followup_webhook=None
    ) -> DiscordPlatformEvent:
        """Creates a Discord message event.

        Args:
            message: AstrBot message object to wrap.
            followup_webhook: Optional slash-command follow-up webhook.

        Returns:
            Created Discord message event.
        """
        return DiscordPlatformEvent(
            message_str=message.message_str,
            message_obj=message,
            platform_meta=self.meta(),
            session_id=message.session_id,
            client=self.client,
            interaction_followup_webhook=followup_webhook,
        )

    async def handle_msg(self, message: AstrBotMessage, followup_webhook=None) -> None:
        """处理消息"""
        message_event = self.create_event(message, followup_webhook)

        if self.client.user is None:
            logger.error(
                "[Discord] Client is not ready (self.client.user is None); message handling skipped"
            )
            return

        # 检查是否为斜杠指令
        is_slash_command = message_event.interaction_followup_webhook is not None

        # 1. 优先处理斜杠指令
        if is_slash_command:
            message_event.is_wake = True
            message_event.is_at_or_wake_command = True
            self.commit_event(message_event)
            return

        # 2. 处理普通消息（提及检测）
        # 确保 raw_message 是 discord.Message 类型，以便静态检查通过
        raw_message = message.raw_message
        if not isinstance(raw_message, discord.Message):
            logger.warning(
                f"[Discord] Non-Message type received and ignored: {type(raw_message)}"
            )
            return

        # 检查是否被@（User Mention 或 Bot 拥有的 Role Mention）
        is_mention = False

        # User Mention
        # 此时 Pylance 知道 raw_message 是 discord.Message，具有 mentions 属性
        if self.client.user in raw_message.mentions:
            is_mention = True

        # Role Mention（Bot 拥有的角色被提及）
        if not is_mention and raw_message.role_mentions:
            bot_member = None
            if raw_message.guild:
                try:
                    bot_member = raw_message.guild.get_member(
                        self.client.user.id,
                    )
                except Exception:
                    bot_member = None
            if bot_member and hasattr(bot_member, "roles"):
                bot_roles = set(bot_member.roles)
                mentioned_roles = set(raw_message.role_mentions)
                if (
                    bot_roles
                    and mentioned_roles
                    and bot_roles.intersection(mentioned_roles)
                ):
                    is_mention = True

        # 如果是被@的消息，设置为唤醒状态
        if is_mention:
            message_event.is_wake = True
            message_event.is_at_or_wake_command = True

        self.commit_event(message_event)

    @override
    async def terminate(self) -> None:
        logger.info("[Discord] Shutting down adapter...")
        self.shutdown_event.set()
        logger.info("[Discord] Cleaning up commands...")
        if self.enable_command_register and self.client:
            try:
                await asyncio.wait_for(
                    self.client.sync_commands(
                        commands=[],
                        guild_ids=[self.guild_id] if self.guild_id else None,
                    ),
                    timeout=10,
                )
                logger.info("[Discord] Commands cleaned up successfully.")
            except Exception as e:
                logger.warning(
                    f"[Discord] Error occurred while cleaning up commands: {e}"
                )

        if self._polling_task:
            self._polling_task.cancel()
            try:
                await asyncio.wait_for(self._polling_task, timeout=10)
            except asyncio.CancelledError:
                logger.info("[Discord] Polling task cancelled successfully.")
            except Exception as e:
                logger.warning(
                    f"[Discord] Error occurred while cancelling polling task: {e}"
                )
        logger.info("[Discord] Closing client connection...")
        if self.client and hasattr(self.client, "close"):
            try:
                await asyncio.wait_for(self.client.close(), timeout=10)
            except Exception as e:
                logger.warning(f"[Discord] Error occurred while closing client: {e}")
        logger.info("[Discord] Adapter shutdown complete.")

    def register_handler(self, handler_info) -> None:
        """注册处理器信息"""
        self.registered_handlers.append(handler_info)

    async def _collect_and_register_commands(self) -> None:
        """收集所有指令并注册到Discord"""
        logger.info("[Discord] Collecting and registering slash commands...")
        registered_commands = []

        for handler_md in star_handlers_registry:
            if not star_map[handler_md.handler_module_path].activated:
                continue
            if not handler_md.enabled:
                continue
            for event_filter in handler_md.event_filters:
                cmd_info = self._extract_command_info(event_filter, handler_md)
                if not cmd_info:
                    continue

                cmd_name, description, cmd_filter_instance = cmd_info

                # 创建动态回调
                callback = self._create_dynamic_callback(cmd_name)

                # 创建一个通用的参数选项来接收所有文本输入
                options = [
                    discord.Option(
                        name="params",
                        description="指令的所有参数",
                        type=discord.SlashCommandOptionType.string,
                        required=False,
                    ),
                ]

                # 创建SlashCommand
                slash_command = discord.SlashCommand(
                    name=cmd_name,
                    description=description,
                    func=callback,
                    options=options,
                    guild_ids=[self.guild_id] if self.guild_id else None,
                )
                self.client.add_application_command(slash_command)
                registered_commands.append(cmd_name)

        if registered_commands:
            logger.info(
                f"[Discord] Ready to sync {len(registered_commands)} commands: {', '.join(registered_commands)}",
            )
        else:
            logger.info("[Discord] No commands found for registration.")

        # 使用 Pycord 的方法同步指令
        # 注意：这可能需要一些时间，并且有频率限制
        try:
            await self.client.sync_commands()
            logger.info("[Discord] Command synchronization completed.")
        except discord.HTTPException as e:
            if self._is_daily_command_quota_error(e):
                logger.warning(
                    "[Discord] Daily application command create quota reached "
                    "(30034); command sync skipped. Existing commands should "
                    "continue to work until the quota resets.",
                )
                return
            logger.warning(f"[Discord] Sync commands failed: {e}")

    @staticmethod
    def _is_daily_command_quota_error(error: discord.HTTPException) -> bool:
        return getattr(error, "code", None) == 30034

    def _create_dynamic_callback(self, cmd_name: str):
        """为每个指令动态创建一个异步回调函数"""

        async def dynamic_callback(
            ctx: discord.ApplicationContext, params: str | None = None
        ) -> None:
            # 1. 嘗試立即响应，防止超时 (移到最前面)
            followup_webhook = None
            try:
                # 設定 2.5 秒超時，避免卡死整個 event loop
                await asyncio.wait_for(ctx.defer(), timeout=2.5)
                followup_webhook = ctx.followup
            except asyncio.TimeoutError:
                logger.warning(
                    f"[Discord] Defer command '{cmd_name}' timeout. Network might be too slow."
                )
                return
            except Exception as e:
                logger.warning(f"[Discord] Failed to defer command '{cmd_name}': {e}")
                return

            # 将平台特定的前缀'/'剥离，以适配通用的CommandFilter
            logger.debug(f"[Discord] Callback triggered: {cmd_name}")
            logger.debug(f"[Discord] Callback context: {ctx}")
            logger.debug(f"[Discord] Callback params: {params}")
            message_str_for_filter = cmd_name
            if params:
                message_str_for_filter += f" {params}"

            logger.debug(
                f"[Discord] Slash command '{cmd_name}' triggered. "
                f"Raw params: '{params}'. "
                f"Built command string: '{message_str_for_filter}'",
            )

            # 2. 构建 AstrBotMessage
            channel = ctx.channel
            abm = AstrBotMessage()
            if channel is not None:
                abm.type = self._get_message_type(channel, ctx.guild_id)
                abm.group_id = self._get_channel_id(channel)
            else:
                # 防守式兜底：channel 取不到时，仍能根据 guild_id/channel_id 推断会话信息
                abm.type = (
                    MessageType.GROUP_MESSAGE
                    if ctx.guild_id is not None
                    else MessageType.FRIEND_MESSAGE
                )
                abm.group_id = str(ctx.channel_id)

            abm.message_str = message_str_for_filter
            abm.sender = MessageMember(
                user_id=str(ctx.author.id),
                nickname=ctx.author.display_name,
            )
            abm.message = [Plain(text=message_str_for_filter)]
            abm.raw_message = ctx.interaction
            abm.self_id = cast(str, self.bot_self_id)
            abm.session_id = str(ctx.channel_id)
            abm.message_id = str(ctx.interaction.id)

            # 3. 将消息和 webhook 分别交给 handle_msg 处理
            await self.handle_msg(abm, followup_webhook)

        return dynamic_callback

    @staticmethod
    def _extract_command_info(
        event_filter: Any,
        handler_metadata: StarHandlerMetadata,
    ) -> tuple[str, str, CommandFilter | None] | None:
        """从事件过滤器中提取指令信息"""
        cmd_name = None
        # is_group = False
        cmd_filter_instance = None

        if isinstance(event_filter, CommandFilter):
            # 暂不支持子指令注册为斜杠指令
            if (
                event_filter.parent_command_names
                and event_filter.parent_command_names != [""]
            ):
                return None
            cmd_name = event_filter.command_name
            cmd_filter_instance = event_filter

        elif isinstance(event_filter, CommandGroupFilter):
            # 暂不支持指令组直接注册为斜杠指令，因为它们没有 handle 方法
            return None

        if not cmd_name:
            return None

        # Discord 斜杠指令名称规范
        if not re.match(r"^[a-z0-9_-]{1,32}$", cmd_name):
            logger.debug(f"[Discord] Skipping invalid slash command format: {cmd_name}")
            return None

        description = handler_metadata.desc or f"Command: {cmd_name}"
        if len(description) > 100:
            description = f"{description[:97]}..."

        return cmd_name, description, cmd_filter_instance
