from __future__ import annotations
import functools
import re
import typing
from typing import Callable, Any, Union, Optional
import telegram as tg
from .tagauth_data import AuthRes

if typing.TYPE_CHECKING:
    from main import Bea

STOP_PROCESSING = True
CONTINUE_PROCESSING = None

class RestartException(SystemExit):
    pass

# A user set, i.e. either a single user 'theezakje' or multiple users '{theezakje Flevosap}'.
# Users may be given with or without @.
USER = r"@?([a-zA-Z0-9_]+)"
USER_SET = r"@?[a-zA-Z0-9_]+|{@?[A-Za-z0-9_]+(?: @?[A-Za-z0-9_]+)*}"

def users_map(bea: Bea, user_str: str, func: Callable, *args: Any, **kwargs: Any) -> None:
    uids = []
    if user_str.startswith('{'):
        users = user_str[1:-1].split()
        for user in users:
            user = user.strip('@').lower()
            if user in bea.known_users:
                uids.append(bea.known_users[user])
    else:
        user = user_str.strip('@').lower()
        if user in bea.known_users:
            uids.append(bea.known_users[user])
    if not uids:
        bea.reply('no known username(s) specified.')
        return

    for uid in uids:
        func(uid, *args, **kwargs)

PREFIX = "(?:(?:Bea[:+] )|\?)"


def ensure_config(config, **kwargs):
    for k, v in kwargs.items():
        if k not in config:
            config[k] = v


def command(regex: Union[re.Pattern, str], *, pass_groups: bool = False, additional_match_fn: Callable = None, bypass_fn: Optional[Callable] = None, inline: bool = False, auth: bool = False, stop: bool = True, prefix: bool = True) -> Callable:
    """
    General-purpose wrapper for command-like functions.

    Arguments:
        regex: Regular expression that message text needs to match in order for the command to be run.

        pass_groups: Whether or not to pass the capture groups extracted from the regex.

        additional_match_fn: An additional function which is run if the regex succeeds, which can extract other data from the Telegram update and pass it to the command. It can also cause the command to not be run, by raising a ValueError.

        bypass_fn: A predicate which, if true, causes the command to be run regardless of whether the regex matches. Note that this does not bypass the additional_match_fn described above.

        inline: Whether or not this command supports being called from an inline query.

        auth: Whether or not this command requires authorization.

        stop: Whether this command, if run, should stop command processing.

        prefix: Whether this command regex should be prepended with a command prefix.
    """

    if not isinstance(regex, re.Pattern):
        if prefix:
            regex = PREFIX + regex
        compiled_regex = re.compile(regex, flags=re.S | re.IGNORECASE)
    else:
        compiled_regex = regex
    def decorator(func: Callable) -> Callable:
        @functools.wraps(func)
        def wrapper(bea: Bea, bot: tg.Bot, update: tg.Update, **kwargs: Any) -> Union[bool, None]:
            msg = update.effective_message
            if not msg and not update.inline_query: return None
            if (not inline and update.inline_query):
                return None
            if (update.inline_query):
                text = update.inline_query.query
            elif update.effective_message:
                text = update.effective_message.caption or update.effective_message.text or ''
            match = compiled_regex.match(text)

            if not (match or (bypass_fn and bypass_fn(update))):
                return CONTINUE_PROCESSING

            groups = match.groups() if match else []
            args = [bea, bot, update]

            if (additional_match_fn):
                try:
                    res = additional_match_fn(update)
                except ValueError:
                    # no match
                    return CONTINUE_PROCESSING
                if isinstance(res, tuple):
                    args.extend(res)
                else:
                    args.append(res)

            if (pass_groups):
                args.append(groups)

            if auth:
                res = bea.try_auth(wrapper, update.effective_user)
                if res == AuthRes.FAIL:
                    try:
                        bea.reply('You are not authorized to perform that command.')
                    finally:
                        return None

            func(*args)
            return STOP_PROCESSING if stop else CONTINUE_PROCESSING
        wrapper._regex = regex        # type: ignore
        wrapper._requiresauth = auth  # type: ignore
        return wrapper
    return decorator

def photo_fn(update: tg.Update) -> Union[tg.PhotoSize, tg.Document]:
    if not update.message:
        raise ValueError('no message?')

    photo_message = update.message.reply_to_message or update.message
    if (not photo_message or not (photo_message.photo or photo_message.document)):
        raise ValueError('no photo found in message or reply-message.')

    if (photo_message.photo):
        return photo_message.photo[-1]
    if (photo_message.document):
        return photo_message.document
    raise ValueError('no photo found in message or reply-message.')

def always(update: tg.Update) -> bool:
    return True

def sticker_reply_fn(update: tg.Update) -> tg.Sticker:
    if not update.message:
        raise ValueError('no message?')

    rtm = update.message.reply_to_message
    if not rtm or not rtm.sticker:
        raise ValueError

    return rtm.sticker

def update_to_message(fn: Callable) -> Callable:
    @functools.wraps(fn)
    def newfn(update: tg.Update) -> Any:
        return fn(update.effective_message)
    return newfn

def reply_fn(update):
    rtm = update.message.reply_to_message
    if not rtm:
        raise ValueError

    return rtm

def audio_fn(update):
    if not update.message:
        raise ValueError('no message?')

    audio_message = update.message.reply_to_message or update.message
    if (not audio_message or not (audio_message.audio)):
        raise ValueError('no audio found in message or reply-message.')

    if (audio_message.audio):
        return audio_message.audio
    raise ValueError('no audio found in message or reply-message.')
