from __future__ import annotations

import re
from dataclasses import dataclass, field, replace
from typing import (
    TYPE_CHECKING,
    Any,
    cast,
)
from re import Match, Pattern
from collections.abc import Iterable, Sequence


from aiogram.filters.base import Filter
from aiogram.types import InlineQuery

if TYPE_CHECKING:
    from magic_filter import MagicFilter
    from aiogram import Bot

InlineCommandPatternType = str | re.Pattern


class InlineCommandException(Exception):
    pass


@dataclass(frozen=True)
class InlineCommandObject:
    command: str = ""
    args: str | None = field(repr=False, default=None)
    regexp_match: Match[str] | None = field(repr=False, default=None)
    """Will be presented match result if the command is presented as regexp in filter"""
    magic_result: Any | None = field(repr=False, default=None)


class InlineCommand(Filter):
    """
    This filter can be helpful for handling commands from the text messages.

    Works only with :class:`aiogram.types.inline_query.InlineQuery` events which have the :code:`text`.
    """

    __slots__ = (
        "commands",
        "ignore_case",
        "magic",
    )

    def __init__(
        self,
        *values: InlineCommandPatternType,
        commands: Sequence[InlineCommandPatternType] | InlineCommandPatternType | None = None,
        ignore_case: bool = False,
        magic: MagicFilter | None = None,
    ) -> None:
        if commands is None:
            commands = []
        if isinstance(commands, (str, re.Pattern)):
            commands = [commands]

        if not isinstance(commands, Iterable):
            msg = (
                "Command filter only supports str, re.Pattern, BotCommand object"
                " or their Iterable"
            )
            raise TypeError(
                msg
            )

        items = []
        for command in (*values, *commands):
            if not isinstance(command, (str, re.Pattern)):
                msg = (
                    "Command filter only supports str, re.Pattern, BotCommand object"
                    " or their Iterable"
                )
                raise TypeError(
                    msg
                )
            item = command.casefold() if ignore_case and isinstance(command, str) else command
            items.append(item)

        if not items:
            msg = "At least one command should be specified"
            raise ValueError(msg)

        self.commands = tuple(items)
        self.ignore_case = ignore_case
        self.magic = magic

    def __str__(self) -> str:
        return self._signature_to_string(
            *self.commands,
            ignore_case=self.ignore_case,
            magic=self.magic,
        )

    def update_handler_flags(self, flags: dict[str, Any]) -> None:
        commands = flags.setdefault("commands", [])
        commands.append(self)

    async def __call__(
        self, event: InlineQuery, bot: Bot
    ) -> bool | dict[str, Any]:
        if not isinstance(event, InlineQuery):
            return False

        text = event.query
        if not text:
            return False

        try:
            command = await self.parse_command(text=text)
        except InlineCommandException:
            return False
        result = {"inline_command": command}
        if command.magic_result and isinstance(command.magic_result, dict):
            result.update(command.magic_result)
        return result

    def extract_command(self, text: str) -> InlineCommandObject:
        try:
            full_command, *args = text.split(maxsplit=1)
        except ValueError as err:
            msg = "not enough values to unpack"
            raise InlineCommandException(msg) from err

        return InlineCommandObject(
            command=full_command,
            args=args[0] if args else None,
        )

    def validate_command(self, command: InlineCommandObject) -> InlineCommandObject:
        for allowed_command in cast("Sequence[InlineCommandPatternType]", self.commands):
            if isinstance(allowed_command, Pattern):  # Regexp
                result = allowed_command.match(command.command)
                if result:
                    return replace(command, regexp_match=result)

            command_name = command.command
            if self.ignore_case:
                command_name = command_name.casefold()

            if command_name == allowed_command:  # String
                return command
        msg = "Command did not match pattern"
        raise InlineCommandException(msg)

    async def parse_command(self, text: str) -> InlineCommandObject:
        command = self.extract_command(text)
        command = self.validate_command(command)
        return self.do_magic(command=command)

    def do_magic(self, command: InlineCommandObject) -> Any:
        if self.magic is None:
            return command
        result = self.magic.resolve(command)
        if not result:
            msg = "Rejected via magic filter"
            raise InlineCommandException(msg)
        return replace(command, magic_result=result)
