from typing import TYPE_CHECKING, Optional, Union

import discord as di
from slidge import MucType
from slixmpp.exceptions import XMPPError

if TYPE_CHECKING:
    from .contact import Contact
    from .group import MUC, Participant
    from .session import Session


MessageableChannel = Union[
    di.TextChannel,
    di.VoiceChannel,
    di.Thread,
    di.DMChannel,
    di.PartialMessageable,
    di.GroupChannel,
    di.PartialMessageable,
    di.StageChannel,
]
Author = Union[di.User, di.Member, di.ClientUser]


async def captcha_handler(
    captcha_required: di.CaptchaRequired, client: "Discord"
) -> str:
    return await client.session.input(
        "You need to complete a captcha to be able to continue using "
        "discord. Maybe you'll find some useful info here:\n"
        f"errors: {captcha_required.errors}\n"
        f"service: {captcha_required.service}\n"
        f"sitekey: {captcha_required.sitekey}\n"
        f"rqdata: {captcha_required.rqdata}\n"
        f"rqtoken: {captcha_required.rqtoken}\n"
        "If you do, you can reply here with the captcha token."
    )


class Discord(di.Client):
    def __init__(self, session: "Session"):
        self.session = session
        super().__init__(captcha_handler=captcha_handler)
        self.log = session.log
        self.ignore_next_msg_event = set[int]()

    def __ignore(self, mid: int):
        if mid in self.ignore_next_msg_event:
            self.ignore_next_msg_event.remove(mid)
            return True
        return False

    async def on_message(self, message: di.Message):
        async with self.session.send_lock:
            if self.__ignore(message.id):
                return

        if sender := await self.get_sender_by_message(message):
            await sender.send_message(message)

    async def on_typing(self, channel: MessageableChannel, user: Author, _when):
        if user == self.user:
            return

        if contact := await self.get_sender(author=user, channel=channel):
            contact.composing()

    async def on_message_edit(self, before: di.Message, after: di.Message):
        if before.content == after.content:
            # edit events are emitted on various occasion,
            # for instance when a thread is created
            return

        if self.__ignore(after.id):
            return

        if sender := await self.get_sender_by_message(after):
            await sender.send_message(after, correction=True)

    async def on_message_delete(self, m: di.Message):
        carbon = m.author == self.user
        if self.__ignore(m.id):
            return

        if isinstance(m.channel, di.DMChannel):
            author = await self.get_sender_by_message(m)
            author.retract(m.id, carbon=carbon)
        elif isinstance(m.channel, (di.TextChannel, di.GroupChannel)):
            muc = await self.session.bookmarks.by_legacy_id(m.channel.id)
            muc.get_system_participant().moderate(m.id)

    async def on_reaction_add(self, reaction: di.Reaction, user: Author):
        await self.update_reactions(reaction, user)

    async def on_reaction_remove(self, reaction: di.Reaction, user: Author):
        await self.update_reactions(reaction, user)

    async def update_reactions(self, reaction: di.Reaction, user: Author):
        message = reaction.message
        channel = message.channel

        if isinstance(message.channel, di.DMChannel):
            if isinstance(user, di.ClientUser):
                await self.session.update_reactions(message)
            else:
                contact = await self.get_contact(user)
                await contact.update_reactions(message)

        elif isinstance(channel, (di.TextChannel, di.GroupChannel)):
            muc = await self.session.bookmarks.by_legacy_id(channel.id)

            if user.id == self.user.id:  # type:ignore
                self.log.debug("ME: %s %s", user, type(user))
                participant = await muc.get_user_participant()
            else:
                self.log.debug("NOT ME: %s %s", user, type(user))
                participant = await muc.get_participant_by_contact(
                    await self.session.contacts.by_legacy_id(user.id)
                )

            await participant.update_reactions(message)

    async def on_presence_update(
        self,
        _before: Union[di.Member, di.Relationship],
        after: Union[di.Member, di.Relationship],
    ):
        if not self.user:
            # should not happen (receiving presences when not logged)
            return

        if after.id == self.user.id:
            # we don't care about self presences
            return

        if isinstance(after, di.Relationship):
            await self.on_friend_presence_update(after)
        elif isinstance(after, di.Member):
            await self.on_guild_presence_update(after)

    async def on_friend_presence_update(self, friend: di.Relationship):
        if not friend.type == di.RelationshipType.friend:
            return
        c = await self.session.contacts.by_discord_user(friend.user)
        c.update_status(friend.status, friend.activity)
        for p in c.participants:
            if p.muc.type == MucType.GROUP:
                p.update_status(friend.status, friend.activity)

    async def on_guild_presence_update(self, member: di.Member):
        guild = member.guild
        # contact = await self.session.contacts.by_discord_user(member.user)
        for channel in guild.channels:
            if not isinstance(channel, di.TextChannel):
                continue
            try:
                muc = await self.session.bookmarks.by_legacy_id(channel.id)
            except XMPPError:
                continue
            participant = await muc.get_participant_by_legacy_id(member.id)
            participant.update_status(member.status, member.activity)

    async def on_relationship_add(self, relationship: di.Relationship):
        contact = await self.get_contact(relationship.user)
        if relationship.type == di.RelationshipType.incoming_request:
            if nick := relationship.nick:
                contact.name = nick
            contact.send_friend_request()
        elif relationship.type == di.RelationshipType.friend:
            contact.is_friend = True
            await contact.add_to_roster()
        else:
            self.log.debug("Ignored relationship: %s", relationship)

    async def on_relationship_update(
        self, _before: di.Relationship, after: di.Relationship
    ):
        await self.on_relationship_add(after)

    async def on_relationship_remove(self, relationship: di.Relationship):
        contact = await self.get_contact(relationship.user)
        contact.is_friend = False

    async def _get_private_muc(self, channel: di.abc.PrivateChannel) -> Optional["MUC"]:
        if not isinstance(channel, di.GroupChannel):
            # "direct message channels" are not something in XMPP, we just have contacts
            return None

        return await self.session.bookmarks.by_legacy_id(channel.id)

    async def on_private_channel_create(self, channel: di.abc.PrivateChannel):
        muc = await self._get_private_muc(channel)
        if muc is not None:
            await muc.add_to_bookmarks()

    async def on_private_channel_update(self, channel: di.abc.PrivateChannel):
        muc = await self._get_private_muc(channel)
        if muc is not None:
            await muc.update_info()

    async def get_contact(self, user: Union[di.User, di.Member]):
        return await self.session.contacts.by_discord_user(user)

    async def get_sender_by_message(self, message: di.Message):
        return await self.get_sender(message.author, message.channel)

    async def get_sender(
        self,
        author: Author,
        channel: MessageableChannel,
    ) -> Optional[Union["Contact", "Participant"]]:
        if isinstance(channel, di.Thread):
            parent = channel.parent
            if isinstance(parent, di.TextChannel):
                channel = parent
            else:
                self.log.debug("Ignoring thread of %s", parent)
                return None

        if isinstance(channel, di.DMChannel):
            if isinstance(author, di.ClientUser):
                return await self.get_contact(channel.recipient)
            else:
                return await self.get_contact(author)

        if isinstance(channel, (di.TextChannel, di.GroupChannel)):
            muc = await self.session.bookmarks.by_legacy_id(channel.id)
            return await muc.get_participant_by_discord_user(author)

        self.log.debug("Could not get the sender %s of %s", author, channel)
        return None
