from pathlib import Path
from typing import TYPE_CHECKING, Optional, Union

import discord as di
import emoji
from slidge.core.mixins.message import ContentMessageMixin
from slidge.core.mixins.presence import PresenceMixin
from slidge.util import lottie, strip_illegal_chars
from slidge.util.types import Avatar, LegacyAttachment, MessageReference
from slidge.util.util import SlidgeLogger

from . import config

if TYPE_CHECKING:
    from .gateway import Gateway
    from .group import MUC
    from .session import Session


class MessageMixin(ContentMessageMixin):
    session: "Session"
    legacy_id: int  # type:ignore
    avatar: Avatar | None
    discord_user: Union[di.User, di.ClientUser]  # type: ignore
    xmpp: "Gateway"
    log: SlidgeLogger

    MARKS = False

    async def update_reactions(self, m: di.Message):
        legacy_reactions = []
        user = self.discord_user
        for r in m.reactions:
            try:
                async for u in r.users():
                    if u.id == user.id:
                        if (
                            r.is_custom_emoji()
                            or not isinstance(r.emoji, str)
                            or not emoji.is_emoji(r.emoji)
                        ):
                            legacy_reactions.append("❓")
                        else:
                            assert isinstance(r.emoji, str)
                            legacy_reactions.append(r.emoji)
            except di.NotFound:
                # the message has now been deleted
                # seems to happen quite a lot. I guess
                # there are moderation bot that are triggered
                # by reactions from users
                # oh, discord…
                return
        self.react(m.id, legacy_reactions)

    async def _get_reference(self, message: di.Message) -> MessageReference[int] | None:
        if not (ref := message.reference):
            return None

        quoted_msg_id = ref.message_id
        if quoted_msg_id is None:
            return None

        reply_to = MessageReference(quoted_msg_id)

        try:
            quoted_msg = await self._resolve_reference_message(message)
        except di.errors.NotFound:
            reply_to.body = "[quoted message could not be found]"
            return reply_to

        if isinstance(quoted_msg, di.DeletedReferencedMessage):
            reply_to.body = "[deleted message]"
            return reply_to

        if isinstance(quoted_msg, str):
            reply_to.body = quoted_msg
            return reply_to

        if (att := quoted_msg.attachments) and (url := att[0].url):
            reply_to.body = att[0].filename + ": " + url
        else:
            reply_to.body = quoted_msg.clean_content
        author = quoted_msg.author
        if author == self.session.discord.user:
            reply_to.author = "user"
            return reply_to

        muc: "MUC" = getattr(self, "muc", None)  # type: ignore
        if muc:
            reply_to.author = await muc.get_participant_by_discord_user(author)
        else:
            reply_to.author = self  # type: ignore

        return reply_to

    async def _resolve_reference_message(
        self, message: di.Message
    ) -> di.Message | di.DeletedReferencedMessage | str:
        assert message.reference is not None
        assert message.reference.message_id is not None

        if message.reference.resolved is not None:
            return message.reference.resolved

        self.log.debug("Attempting to fetch referenced message…")

        if message.type == di.MessageType.thread_starter_message:
            assert isinstance(message.channel, di.Thread)
            assert isinstance(message.channel.parent, di.TextChannel)
            return await message.channel.parent.fetch_message(
                message.reference.message_id
            )

        channel = self.session.discord.get_channel(message.reference.channel_id)
        if channel is None:
            return "[quoted message from an unknown channel]"

        if isinstance(
            channel,
            (di.ForumChannel, di.DirectoryChannel, di.CategoryChannel, di.DMChannel),
        ):
            return f"[quoted message from an unsupported channel type: {type(channel)}]"

        return await channel.fetch_message(message.reference.message_id)

    def _get_forwarded_text(
        self, reference: di.MessageReference, reply_to: MessageReference[int]
    ) -> str:
        if reply_to.author is None:
            source = "unknown author in "
        elif reply_to.author == "user":
            source = "yourself in "
        else:
            name = (
                reply_to.author.nickname  # type:ignore[union-attr]
                if reply_to.author.name
                is NotImplemented  # for participants, something ugly to fix in slidge cord
                else reply_to.author.name  # for contacts
            )
            source = f"{name} in "

        if reference.guild_id:
            guild = self.session.discord.get_guild(reference.guild_id)
            if guild is None:
                source += "<unknown server>/"
            else:
                source += f"{guild.name}/"
        channel = self.session.discord.get_channel(reference.channel_id)

        if channel is None:
            source += "unknown channel"
        elif isinstance(channel, di.DMChannel):
            source += "direct message"
        else:
            source += channel.name or f"<unnamed <{type(channel)}>>"

        text = f"↱ Forwarded from {source} <{reference.jump_url}>\n"
        assert reply_to.body is not None
        text += "\n".join(("> " + x).strip() for x in reply_to.body.split("\n")).strip()
        return text

    async def send_message(
        self, message: di.Message, archive_only=False, correction=False
    ):
        reply_to = await self._get_reference(message)

        mtype = message.type
        if mtype == di.MessageType.thread_created:
            text: str | None = f"/me created a thread named '{message.clean_content}'"
        elif mtype == di.MessageType.thread_starter_message:
            text = "I started a new thread from this message ↑"
        elif mtype != di.MessageType.reply and reply_to is not None:
            # forwarded message
            assert message.reference is not None
            assert reply_to is not None
            text = self._get_forwarded_text(message.reference, reply_to)
            reply_to = None
        else:
            text = message.clean_content

        channel = message.channel
        if isinstance(channel, di.Thread):
            thread = channel.id
            if message.type == di.MessageType.channel_name_change:
                text = f"/me renamed this thread: {text}"
        else:
            thread = None

        # it seems attachments cannot be edited in discord anyway, only the text
        # of the message
        attachments = (
            [Attachment.from_discord(a) for a in message.attachments]
            if not correction
            else []
        )

        attachments.extend(await self._handle_stickers(message.stickers))

        gifs = await self._handle_embeds(message.embeds)
        if gifs:
            # in this case the text is actually just the URL, we do not want to pass
            # that as the "body" of this message, it's already embedded in the
            # attachment
            text = None
            attachments = gifs

        if (
            not attachments
            and message.clean_content.startswith(
                "https://cdn.discordapp.com/attachments/"
            )
            and len(message.clean_content.strip().split()) == 1
        ):
            attachments = [Attachment(url=message.clean_content.strip())]
            text = None

        await self.send_files(
            attachments,
            body_first=True,
            legacy_msg_id=message.id,
            when=message.created_at,
            thread=thread,
            body=text,
            reply_to=reply_to,
            archive_only=archive_only,
            carbon=message.author == self.session.discord.user,
            correction=correction,
        )

    async def _handle_stickers(
        self, stickers: list[di.StickerItem]
    ) -> list["LegacyAttachment"]:
        return [await self._handle_sticker(sticker) for sticker in stickers]

    async def _handle_sticker(self, sticker: di.StickerItem) -> "LegacyAttachment":
        if sticker.format == di.StickerFormatType.lottie:
            try:
                return await self._convert_sticker(sticker)
            except Exception as e:
                self.log.error("Could not convert sticker!", exc_info=e)

        return Attachment(
            url=sticker.url,
            name="sticker" + str(sticker.id) + "." + sticker.format.file_extension,
            legacy_file_id="sticker-" + str(sticker.id),
            disposition="inline",
        )

    async def _convert_sticker(self, sticker: di.StickerItem) -> "LegacyAttachment":
        sticker_id = str(sticker.id)
        return await lottie.from_url(sticker.url, sticker_id, self.session.http)

    async def _handle_embeds(self, embeds: list[di.Embed]) -> list["Attachment"]:
        attachments = []
        for embed in embeds:
            if embed.provider.name == "Tenor":
                attachments.append(
                    Attachment(
                        url=embed.video.url,
                        legacy_file_id=embed.url,
                        disposition="inline",
                    )
                )
        return attachments


class StatusMixin(PresenceMixin):
    def update_status(
        self,
        status: di.Status,
        activity: Optional[
            Union[di.Activity, di.Game, di.CustomActivity, di.Streaming, di.Spotify]
        ],
    ):
        # TODO: implement timeouts for activities (the Activity object has timestamps
        #       attached to it)
        msg = self.activity_to_text(activity)
        if status == di.Status.online:
            self.online(msg)
        elif status == di.Status.offline:
            self.extended_away(msg)
        elif status == di.Status.idle:
            self.away(msg)
        elif status == di.Status.dnd:
            self.busy(msg)

    @staticmethod
    def activity_to_text(
        activity: Optional[
            Union[di.Activity, di.Game, di.CustomActivity, di.Streaming, di.Spotify]
        ],
    ) -> Optional[str]:
        if isinstance(activity, di.Game):
            return strip_illegal_chars(f"Playing {activity.name}")
        elif isinstance(activity, di.Streaming):
            text = f"Streaming at {activity.url}"
            if name := activity.name:
                text += f" - {name}"
            if game := activity.game:
                text += f" - {game}"
            return strip_illegal_chars(text)
        elif isinstance(activity, di.Spotify):
            return strip_illegal_chars(
                f"Listening to {activity.title} by {activity.artist} "
                f"({activity.album}) - {activity.track_url}"
            )
        elif isinstance(activity, di.CustomActivity):
            text = ""
            if name := activity.name:
                text += name
            if e := activity.emoji:
                text += f" <{e.url}>"
            return strip_illegal_chars(text.lstrip()) or None
        elif isinstance(activity, di.Activity):
            text = ""
            if name := activity.name:
                text += name
            if state := activity.state:
                text += f" - {state}"
            if e := activity.emoji:
                text += f" <{e.url}>"
            if url := activity.large_image_url:
                text += f" - <{url}>"
            if type_ := activity.type:
                type_string = str(type_).removeprefix("ActivityType.")
                if "unknown" not in type_string:
                    text += f" - {type_string}"
            return strip_illegal_chars(text.lstrip()) or None
        return None


class Attachment(LegacyAttachment):
    @staticmethod
    def from_discord(di_attachment: di.Attachment):
        return Attachment(
            url=di_attachment.url,
            name=di_attachment.filename,
            content_type=di_attachment.content_type,
            legacy_file_id=di_attachment.id,
            caption=di_attachment.description or None,
        )
