import uuid
import warnings
from collections.abc import Iterable
from datetime import UTC, datetime
from typing import TYPE_CHECKING, cast

from slixmpp import JID, Message
from slixmpp.types import MessageTypes

from slidge.util import strip_illegal_chars

from ...db.models import GatewayUser
from ...slixfix.link_preview.stanza import LinkPreview as LinkPreviewStanza
from ...util.types import (
    ChatState,
    LegacyMessageType,
    LinkPreview,
    MessageReference,
    ProcessingHint,
)
from .. import config
from .base import BaseSender

if TYPE_CHECKING:
    from ...group import LegacyMUC, LegacyParticipant


class MessageMaker(BaseSender):
    mtype: MessageTypes = NotImplemented
    _can_send_carbon: bool = NotImplemented
    STRIP_SHORT_DELAY = False
    USE_STANZA_ID = False

    muc: "LegacyMUC"

    def _recipient_pk(self) -> int:
        return (
            self.muc.stored.id if self.is_participant else self.stored.id  # type:ignore
        )

    def _make_message(
        self,
        state: ChatState | None = None,
        hints: Iterable[ProcessingHint] = (),
        legacy_msg_id: LegacyMessageType | None = None,
        when: datetime | None = None,
        reply_to: MessageReference | None = None,
        carbon: bool = False,
        link_previews: Iterable[LinkPreview] | None = None,
        **kwargs,
    ):
        body = kwargs.pop("mbody", None)
        mfrom = kwargs.pop("mfrom", self.jid)
        mto = kwargs.pop("mto", None)
        thread = kwargs.pop("thread", None)
        if carbon and self._can_send_carbon:
            # the msg needs to have jabber:client as xmlns, so
            # we don't want to associate with the XML stream
            msg_cls = Message
        else:
            msg_cls = self.xmpp.Message  # type:ignore
        msg = msg_cls(
            sfrom=mfrom,
            stype=kwargs.pop("mtype", None) or self.mtype,
            sto=mto,
            **kwargs,
        )
        if body:
            msg["body"] = strip_illegal_chars(body, "�")
            state = "active"
        if thread:
            with self.xmpp.store.session() as orm:
                thread_str = str(thread)
                msg["thread"] = (
                    self.xmpp.store.id_map.get_thread(
                        orm,
                        self._recipient_pk(),
                        thread_str,
                        self.is_participant,
                    )
                    or thread_str
                )
        if state:
            msg["chat_state"] = state
        for hint in hints:
            msg.enable(hint)
        self._set_msg_id(msg, legacy_msg_id)
        self._add_delay(msg, when)
        if link_previews:
            self._add_link_previews(msg, link_previews)
        if reply_to:
            self._add_reply_to(msg, reply_to)
        return msg

    def _set_msg_id(
        self, msg: Message, legacy_msg_id: LegacyMessageType | None = None
    ) -> None:
        if legacy_msg_id is not None:
            i = self.session.legacy_to_xmpp_msg_id(legacy_msg_id)
            msg.set_id(i)
            if self.USE_STANZA_ID:
                msg["stanza_id"]["id"] = i
                msg["stanza_id"]["by"] = self.muc.jid  # type: ignore
        elif self.USE_STANZA_ID:
            msg["stanza_id"]["id"] = str(uuid.uuid4())
            msg["stanza_id"]["by"] = self.muc.jid  # type: ignore

    def _legacy_to_xmpp(self, legacy_id: LegacyMessageType) -> list[str]:
        with self.xmpp.store.session() as orm:
            ids = self.xmpp.store.id_map.get_xmpp(
                orm,
                self._recipient_pk(),
                str(legacy_id),
                self.is_participant,
            )
            if ids:
                return ids
        return [self.session.legacy_to_xmpp_msg_id(legacy_id)]

    def _add_delay(self, msg: Message, when: datetime | None) -> None:
        if when:
            if when.tzinfo is None:
                when = when.astimezone(UTC)
            if self.STRIP_SHORT_DELAY:
                delay = (datetime.now().astimezone(UTC) - when).seconds
                if delay < config.IGNORE_DELAY_THRESHOLD:
                    return
            msg["delay"].set_stamp(when)
            msg["delay"].set_from(self.xmpp.boundjid.bare)

    def _add_reply_to(self, msg: Message, reply_to: MessageReference) -> None:
        xmpp_id = self._legacy_to_xmpp(reply_to.legacy_id)[0]
        msg["reply"]["id"] = xmpp_id

        muc = getattr(self, "muc", None)

        if entity := reply_to.author:
            if entity == "user" or isinstance(entity, GatewayUser):
                if isinstance(entity, GatewayUser):
                    warnings.warn(
                        "Using a GatewayUser as the author of a "
                        "MessageReference is deprecated. Use the string 'user' "
                        "instead.",
                        DeprecationWarning,
                    )
                if muc:
                    jid = JID(muc.jid)
                    jid.resource = fallback_nick = muc.user_nick
                    msg["reply"]["to"] = jid
                else:
                    msg["reply"]["to"] = self.session.user_jid
                    # TODO: here we should use preferably use the PEP nick of the user
                    # (but it doesn't matter much)
                    fallback_nick = self.session.user_jid.user
            else:
                if muc:
                    if hasattr(entity, "muc"):
                        # TODO: accept a Contact here and use muc.get_participant_by_legacy_id()
                        # a bit of work because right now this is a sync function
                        entity = cast("LegacyParticipant", entity)
                        fallback_nick = entity.nickname
                    else:
                        warnings.warn(
                            "The author of a message reference in a MUC must be a"
                            " Participant instance, not a Contact"
                        )
                        fallback_nick = entity.name
                else:
                    fallback_nick = entity.name
                msg["reply"]["to"] = entity.jid
        else:
            fallback_nick = None

        if fallback := reply_to.body:
            msg["reply"].add_quoted_fallback(fallback, fallback_nick)

    @staticmethod
    def _add_link_previews(msg: Message, link_previews: Iterable[LinkPreview]) -> None:
        for preview in link_previews:
            element = LinkPreviewStanza()
            for i, name in enumerate(preview._fields):
                val = preview[i]
                if not val:
                    continue
                element[name] = val
            msg.append(element)
