from typing import TYPE_CHECKING, Union

import discord as di
from slidge import LegacyContact, LegacyRoster
from slidge.util.types import Avatar
from slixmpp.exceptions import XMPPError

from . import config
from .util import MessageMixin, StatusMixin

if TYPE_CHECKING:
    from .group import Participant
    from .session import Session


class Contact(StatusMixin, MessageMixin, LegacyContact[int]):  # type: ignore
    session: "Session"
    participants: set["Participant"]  # type:ignore

    # not needed because discord.py-self throws separate presence events
    # per guild
    PROPAGATE_PRESENCE_TO_GROUPS = False

    @property
    def discord_user(self) -> di.User:  # type: ignore
        self.session.log.debug("Searching for user: %s", self.legacy_id)
        user = self.session.discord.get_user(self.legacy_id)
        # self.session.discord.get_guild().get_member()
        if user is None:
            raise XMPPError(
                "item-not-found", text=f"Cannot find the discord user {self.legacy_id}"
            )
        return user

    @property
    def direct_channel_id(self):
        assert self.discord_user.dm_channel is not None
        return self.discord_user.dm_channel.id

    async def update_info(self):
        u = self.discord_user
        if u.bot or u.system:
            self.client_type = "bot"
        self.name = u.display_name
        if a := u.avatar:
            self.avatar = Avatar(url=a.url, unique_id=a.key)
        else:
            self.avatar = None

        self.is_friend = u.is_friend()

        # massive rate limiting if trying to fetch profiles of non friends
        if config.AUTO_FETCH_FRIENDS_BIO and self.is_friend:
            await self.fetch_vcard()

        relationship = u.relationship
        if relationship is None:
            return

        if relationship.type == di.RelationshipType.incoming_request:
            self.send_friend_request()

    async def fetch_vcard(self):
        try:
            profile = await self.discord_user.profile()
        except di.Forbidden:
            self.session.log.debug("Forbidden to fetch the profile of %s", self)
        except di.HTTPException as e:
            self.session.log.debug(
                "HTTP exception %s when fetch the profile of %s", e, self
            )
        else:
            profile_bio = profile.bio
            metadata_bio = profile.metadata.bio
            if profile_bio == metadata_bio:
                bio = profile_bio
            else:
                bio = "\n\n".join(
                    [x for x in (profile_bio, metadata_bio) if x is not None]
                ).strip()
            self.set_vcard(
                full_name=self.name,
                note=bio or None,
                pronouns=profile.metadata.pronouns,
            )

    async def on_friend_accept(self):
        relationship = self.discord_user.relationship
        if (
            relationship is None
            or relationship.type != di.RelationshipType.incoming_request
        ):
            raise XMPPError(
                "bad-request",
                f"You accepted a friend request, but the discord relationship is {relationship}",
            )

        await relationship.accept()

    async def on_friend_delete(self, text=""):
        relationship = self.discord_user.relationship
        if not self.discord_user.is_friend():
            raise XMPPError(
                "bad-request",
                f"You deleted a friend, but the discord relationship is {relationship}",
            )

        assert relationship is not None
        await relationship.delete()

    async def on_friend_request(self, text=""):
        if self.discord_user.is_friend():
            raise XMPPError(
                "bad-request",
                f"You sent a friend request, but the discord relationship is {self.discord_user.relationship}",
            )

        try:
            await self.session.discord.send_friend_request(self.discord_user)
        except di.Forbidden as exc:
            raise XMPPError("forbidden", exc.text)


class Roster(LegacyRoster[int, Contact]):
    session: "Session"

    async def by_discord_user(self, u: Union[di.User, di.Member]) -> Contact:
        return await self.by_legacy_id(u.id)

    async def jid_username_to_legacy_id(self, username: str):
        try:
            user_id = int(username)
        except ValueError:
            raise XMPPError(
                "bad-request",
                text=f"Not a valid discord ID: {username}",
            )
        else:
            if self.session.discord.get_user(user_id) is None:
                self.session.log.debug(
                    "I could not find the JID local part %s", username
                )
                raise XMPPError(
                    "item-not-found",
                    text=f"No discord user was found with ID: {username}",
                )
            return user_id

    async def legacy_id_to_jid_username(self, discord_user_id: int) -> str:
        return str(discord_user_id)

    async def fill(self):
        for relationship in self.session.discord.friends:
            u = relationship.user
            self.session.log.debug("Friend: %r", u)
            if not isinstance(u, di.User):
                self.session.log.debug("Skipping %s", u)
                continue
            c = await self.by_legacy_id(u.id)
            c.update_status(relationship.status, relationship.activity)
            yield c
