import typing
from collections.abc import AsyncIterator

import nio
from slidge.group import LegacyBookmarks, LegacyMUC, LegacyParticipant, MucType
from slidge.util.types import HoleBound
from slixmpp import Presence
from slixmpp.exceptions import XMPPError

from . import config
from .util import MatrixMixin, server_timestamp_to_datetime

if typing.TYPE_CHECKING:
    from .session import Session


class Participant(MatrixMixin, LegacyParticipant):
    session: "Session"
    muc: "MUC"

    def set_affiliation_from_power_level(self, power_level: int) -> None:
        if power_level == 100:
            self.affiliation = "owner"
            self.role = "moderator"
        elif power_level >= 50:
            self.affiliation = "admin"
            self.role = "moderator"
        else:
            self.affiliation = "member"
            self.role = "participant"


class Bookmarks(LegacyBookmarks):
    session: "Session"

    async def fill(self) -> None:
        self.log.debug("Filling rooms")
        self.log.debug(f"Joined rooms: {self.session.matrix.rooms.keys()}")
        self.log.debug(f"Invited rooms: {self.session.matrix.invited_rooms.keys()}")
        for room in self.session.matrix.rooms:
            try:
                muc = await self.by_legacy_id(room)
                await muc.add_to_bookmarks()
                self.log.debug(f"Successfully processed room {room}: {muc}")
            except XMPPError as e:
                self.log.debug(
                    "%s is not a group chat or trouble getting it: %r", room, e
                )

        # Relay Matrix invitations as XMPP room invites.
        for room_id in self.session.matrix.invited_rooms:
            try:
                muc = await self.by_legacy_id(room_id)
                self.log.debug(f"Relaying Matrix invitation to XMPP for {room_id}")
                self.session.send_gateway_invite(muc)
            except XMPPError as e:
                self.log.debug("Failed to relay invitation for %s: %r", room_id, e)


class MUC(LegacyMUC[str, str, Participant, str]):
    session: "Session"
    type = MucType.CHANNEL_NON_ANONYMOUS

    async def get_room(self) -> nio.MatrixRoom:
        if room := self.session.matrix.rooms.get(self.legacy_id):
            return room
        # FIXME: invited_rooms is not persisted across restarts.
        if room := self.session.matrix.invited_rooms.get(self.legacy_id):
            return room
        raise XMPPError("item-not-found", f"No room named {self.legacy_id}")

    async def get_message(self, msg_id: str) -> nio.Event | None:
        return await self.session.matrix.get_event(self.legacy_id, msg_id)

    async def update_info(self) -> None:
        room = await self.get_room()

        if new := room.replacement_room:
            localpart = await self.session.bookmarks.legacy_id_to_jid_username(new)
            raise XMPPError(
                "redirect", f"xmpp:{localpart}@{self.xmpp.boundjid.bare}?join"
            )
        self.log.debug("Children: %s", room.children)
        if room.children:
            raise XMPPError("bad-request", "This is not a real room but a 'space'")

        self.user_nick = room.user_name(self.session.matrix.user_id)

        self.log.debug("User nick: %s", self.user_nick)
        self.name = room.display_name
        self.log.debug("Avatar: %s", room.room_avatar_url)
        self.n_participants = room.member_count
        self.subject = room.topic

        if room.room_avatar_url:
            self.avatar = await self.session.matrix.mxc_to_http(room.room_avatar_url)

        elif room.member_count == 2:
            # if 1:1 set room avatar to other users's avatar
            for user_id, user in list(room.users.items()):
                if user_id == self.session.matrix.user_id:
                    continue
                if user.avatar_url is None:
                    break
                self.avatar = await self.session.matrix.mxc_to_http(user.avatar_url)
                break

    async def fill_participants(self) -> AsyncIterator[Participant]:
        room = await self.get_room()
        if not room.members_synced:
            resp = await self.session.matrix.joined_members(self.legacy_id)
            if isinstance(resp, nio.JoinedMembersResponse):
                self.log.debug(
                    "Joined members response: %s participants", len(resp.members)
                )
            else:
                self.log.debug("Joined members error: %s", resp)

        power_levels = room.power_levels.users
        i = 0

        for user_id, user in list(room.users.items()):
            power_level = power_levels.get(user_id, 0)
            if power_level < 50 and i > config.MAX_PARTICIPANTS_FETCH:
                continue
            try:
                p = await self.get_participant_by_legacy_id(user.user_id)
            except XMPPError:
                continue
            p.set_affiliation_from_power_level(power_level)
            if power_level < 50:
                i += 1
            yield p

    async def backfill(
        self,
        after: HoleBound | None = None,
        before: HoleBound | None = None,
    ) -> None:
        after_date = None if after is None else after.timestamp
        before_date = None if before is None else before.timestamp
        self.log.info("Backfilling from {%s} until {%s}.", after_date, before_date)

        total = 0
        undecryptable = 0
        skipped = 0
        filled = 0
        reactions = 0

        async for event in self.session.matrix.fetch_history(self.legacy_id):
            total += 1
            if not isinstance(event, (nio.RoomMessage | nio.ReactionEvent)):
                self.log.debug("Not back-filling with %s", type(event))
                if isinstance(event, nio.MegolmEvent):
                    undecryptable += 1
                continue

            when = server_timestamp_to_datetime(event)
            if after_date is not None and when <= after_date:
                # Events are yielded in reverse chronological order.
                # If this one is too old, then all subsequent ones are too.
                break
            if before_date is not None and when >= before_date:
                continue

            try:
                participant = await self.session.matrix.get_participant(
                    await self.get_room(), event
                )
            except XMPPError as e:
                # Maybe deleted profiles?
                self.log.warning(
                    "Failed to fetch participant %s, using matrix ID as nickname: %s",
                    event.sender,
                    e,
                )
                participant = await self.get_participant(event.sender)

            if isinstance(event, nio.RoomMessage):
                await participant.send_matrix_message(event, archive_only=True)
                filled += 1
            elif isinstance(event, nio.ReactionEvent):
                msg_id = await self.session.matrix.get_original_id(
                    self.legacy_id,
                    event.reacts_to,
                )
                participant.react(msg_id, [event.key], archive_only=True)
                reactions += 1

            if filled >= config.MAX_HISTORY_FETCH:
                break

        self.log.info(
            "Backfilled: %d events, %d filled, %d reactions, %d undecryptable, %d unhandled.",
            total,
            filled,
            reactions,
            undecryptable,
            skipped,
        )

    async def join(self, join_presence: Presence) -> None:
        """Called when user joins the room. Join on Matrix side if invited."""
        response = await self.session.matrix.join(self.legacy_id)
        if isinstance(response, nio.JoinResponse):
            self.log.debug(f"Successfully joined Matrix room {self.legacy_id}")
        else:
            self.log.warning(f"Failed to join Matrix room {self.legacy_id}: {response}")
        await super().join(join_presence)
