import asyncio
import re
from typing import Iterable, Set, Tuple

from nonebot.adapters.cqhttp.message import Message, MessageSegment
from nonebot.adapters.cqhttp.utils import escape, unescape

from ..poly import Event as CQEvent
from . import NoneBot
from .command import SwitchException, handle_command
from .log import logger
from .natural_language import handle_natural_language
from .plugin import PluginManager
from .typing import MessagePreprocessor_T

_message_preprocessors: Set[MessagePreprocessor_T] = set()


def message_preprocessor(func: MessagePreprocessor_T) -> MessagePreprocessor_T:
    _message_preprocessors.add(func)
    return func


class CanceledException(Exception):
    """
    Raised by message_preprocessor indicating that
    the bot should ignore the message
    """

    def __init__(self, reason):
        """
        :param reason: reason to ignore the message
        """
        self.reason = reason


async def handle_message(bot: NoneBot, event: CQEvent) -> None:
    """INTERNAL API"""
    _log_message(event)

    assert isinstance(event.message, Message)
    if not event.message:
        event.message.append(MessageSegment.text(""))  # type: ignore

    raw_to_me = event.get("to_me", False)
    _check_at_me(bot, event)
    _check_calling_me_nickname(bot, event)
    event["to_me"] = raw_to_me or event["to_me"]

    coros = []
    plugin_manager = PluginManager()
    for preprocessor in _message_preprocessors:
        coros.append(preprocessor(bot, event, plugin_manager))
    if coros:
        try:
            await asyncio.gather(*coros)
        except CanceledException as e:
            logger.info(f'Message {event["message_id"]} is ignored: {e.reason}')
            return

    while True:
        try:
            handled = await handle_command(bot, event, plugin_manager.cmd_manager)
            break
        except SwitchException as e:
            # we are sure that there is no session existing now
            event["message"] = e.new_message
            event["to_me"] = True
    if handled:
        logger.info(f"Message {event.message_id} is handled as a command")
        return

    handled = await handle_natural_language(bot, event, plugin_manager.nlp_manager)
    if handled:
        logger.info(f"Message {event.message_id} is handled " f"as natural language")
        return


def _check_at_me(bot: NoneBot, event: CQEvent) -> None:
    assert event.message is not None
    if event.detail_type == "private":
        event["to_me"] = True
    else:
        # group or discuss
        event["to_me"] = False
        at_me_seg = MessageSegment.at(event.self_id)

        # check the first segment
        first_msg_seg = event.message[0]
        if first_msg_seg == at_me_seg:
            event["to_me"] = True
            del event.message[0]

        if not event["to_me"]:
            # check the last segment
            i = -1
            last_msg_seg = event.message[i]
            if (
                last_msg_seg.type == "text"
                and not last_msg_seg.data["text"].strip()
                and len(event.message) >= 2
            ):
                i -= 1
                last_msg_seg = event.message[i]

            if last_msg_seg == at_me_seg:
                event["to_me"] = True
                del event.message[i:]

        if not event.message:
            event.message.append(MessageSegment.text(""))


def _check_calling_me_nickname(bot: NoneBot, event: CQEvent) -> None:
    first_msg_seg = event.message[0]  # type:ignore
    if first_msg_seg.type != "text":
        return

    first_text: str = first_msg_seg.data["text"]

    if bot.config.NICKNAME:
        nicknames: Tuple[str, ...]
        # check if the user is calling me with my nickname
        if isinstance(bot.config.NICKNAME, str) or not isinstance(
            bot.config.NICKNAME, Iterable
        ):
            nicknames = (bot.config.NICKNAME,)
        else:
            nicknames = tuple(filter(lambda n: n, bot.config.NICKNAME))
        nickname_regex = "|".join(nicknames)
        m = re.search(rf"^({nickname_regex})([\s,，]*|$)", first_text, re.IGNORECASE)
        if m:
            nickname = m.group(1)
            logger.debug(f"User is calling me {nickname}")
            event["to_me"] = True
            first_msg_seg.data["text"] = first_text[m.end() :]


def _log_message(event: CQEvent) -> None:
    msg_from = str(event.user_id)
    if event.detail_type == "group":
        msg_from += f"@[群:{event.group_id}]"
    elif event.detail_type == "discuss":
        msg_from += f"@[讨论组:{event.discuss_id}]"
    logger.info(
        f"Self: {event.self_id}, "
        f"Message {event.message_id} from {msg_from}: "
        f"{repr(str(event.message))}"
    )


__all__ = [
    "message_preprocessor",
    "CanceledException",
    "Message",
    "MessageSegment",
    "escape",
    "unescape",
]
