import collections
import inspect
import re
from threading import Thread

from core.aochat import server_packets
from core.chat_blob import ChatBlob
from core.command_request import CommandRequest
from core.conn import Conn
from core.decorators import instance
from core.dict_object import DictObject
from core.functions import flatmap, get_attrs
from core.logger import Logger
from core.lookup.character_service import CharacterService
from core.message_hub_service import MessageHubService
from core.registry import Registry
from core.sender_obj import SenderObj
from core.setting_service import SettingService
from modules.core.accounting.services.access_service import AccessService


# noinspection SqlResolve,PyAttributeOutsideInit,PyUnresolvedReferences,PyTypeChecker
@instance()
class CommandService:
    PRIVATE_CHANNEL = "priv"
    ORG_CHANNEL = "org"
    PRIVATE_MESSAGE_CHANNEL = "msg"

    def __init__(self):
        self.ignore = []
        self.handlers = collections.defaultdict(list)
        self.logger = Logger(__name__)
        self.channels = {}
        self.pre_processors = []
        self.ignore_regexes = [
            re.compile(r" is AFK \(Away from keyboard\) since ", re.IGNORECASE),
            re.compile(r"I am away from my keyboard right now", re.IGNORECASE),
            re.compile(r"Unknown command or access denied!", re.IGNORECASE),
            re.compile(r"I am responding", re.IGNORECASE),
            re.compile(r"I only listen", re.IGNORECASE),
            re.compile(r"Error!", re.IGNORECASE),
            re.compile(r"Unknown command input", re.IGNORECASE),
            re.compile(r"You have been auto invited", re.IGNORECASE),
            re.compile(r"^<font")
        ]

    def inject(self, registry):
        self.db = registry.get_instance("db")
        self.util = registry.get_instance("util")
        self.access_service: AccessService = registry.get_instance("access_service")
        self.bot = registry.get_instance("bot")
        self.character_service: CharacterService = registry.get_instance("character_service")
        self.setting_service: SettingService = registry.get_instance("setting_service")
        self.command_alias_service = registry.get_instance("command_alias_service")
        self.usage_service = registry.get_instance("usage_service")
        self.public_channel_service = registry.get_instance("public_channel_service")
        self.ban_service = registry.get_instance("ban_service")
        self.getresp = registry.get_instance("translation_service").get_response
        self.relay_hub_service: MessageHubService = registry.get_instance("message_hub_service")

    def pre_start(self):
        self.bot.register_packet_handler(server_packets.PrivateMessage.id, self.handle_private_message)
        self.bot.register_packet_handler(server_packets.PrivateChannelMessage.id, self.handle_private_channel_message)
        # Org channel is not supported
        self.bot.register_packet_handler(server_packets.PublicChannelMessage.id, self.handle_public_channel_message)
        self.register_command_channel("Private Message", self.PRIVATE_MESSAGE_CHANNEL)
        self.register_command_channel("Org Channel", self.ORG_CHANNEL)
        self.register_command_channel("Private Channel", self.PRIVATE_CHANNEL)
        self.relay_hub_service.register_message_source("tell_logger")
        self.relay_hub_service.register_message_source("system_logger")
        self.relay_hub_service.register_message_source("access_denied_logger")
        self.relay_hub_service.register_message_source("member_logger")

    def start(self):
        access_levels = {}

        # process decorators
        for _, inst in Registry.get_all_instances().items():
            for name, method in get_attrs(inst).items():
                if hasattr(method, "command"):
                    key = Registry.get_module_name(inst).split(".")
                    if key[0] not in self.bot.modules:
                        continue
                    cmd_name, params, access_level, description, help_file, sub_command, extended_description = getattr(
                        method, "command")
                    handler = getattr(inst, name)
                    help_text = self.get_help_file(inst.module_name, help_file)

                    command_key = self.get_command_key(cmd_name.lower(), sub_command.lower() if sub_command else "")
                    al = access_levels.get(command_key, None)
                    if al is not None and al != access_level.lower():
                        print(handler)
                        raise Exception(f"Different access levels specified for forms of command '{command_key}'")
                    access_levels[command_key] = access_level

                    self.register(handler, cmd_name, params, access_level, description, inst.module_name, help_text,
                                  sub_command, extended_description)

    def register(self, handler, command, params, access_level, description, module, help_text=None, sub_command=None,
                 extended_description=None, check_access=None):
        """
        Call during pre_start

        Args:
            handler: (request, param1, param2, ...) -> str|ChatBlob|None
            command: str
            params: [CommandParam...]
            access_level: str
            description: str
            module: str
            help_text: str
            sub_command: str
            extended_description: str
            check_access: (char, access_level_label) -> bool
        """

        if len(inspect.signature(handler).parameters) != len(params) + 1:
            raise Exception(
                "Incorrect number of arguments for handler '%s.%s()'" % (handler.__module__, handler.__name__))

        command = command.lower()
        if sub_command:
            sub_command = sub_command.lower()
        else:
            sub_command = ""
        access_level = access_level.lower()
        module = module.lower()
        command_key = self.get_command_key(command, sub_command)

        if help_text is None:
            help_text = self.generate_help(command, description, params, extended_description)

        if check_access is None:
            check_access = self.access_service.check_access

        if not self.access_service.get_access_level_by_label(access_level):
            self.logger.error("Could not add command '%s': could not find access level '%s'" % (command, access_level))
            return

        for channel, label in self.channels.items():
            row = self.db.query_single("SELECT access_level, module, enabled, verified, sub_command "
                                       "FROM command_config "
                                       "WHERE command = ? AND sub_command = ? AND channel = ?",
                                       [command, sub_command, channel])

            if row is None:
                # add new command
                self.db.exec(
                    "INSERT INTO command_config "
                    "(command, sub_command, access_level, channel, module, enabled, verified) "
                    "VALUES (?, ?, ?, ?, ?, 1, 1)",
                    [command, sub_command, access_level, channel, module])
            elif row.verified:
                if row.module != module and row.sub_command == sub_command:
                    self.logger.warning(f"module different for different forms of command '{command}' "
                                        f"and sub_command '{sub_command}'")
            else:
                # mark command as verified
                self.db.exec("UPDATE command_config SET verified = 1, module = ? "
                             "WHERE command = ? AND sub_command = ? AND channel = ?",
                             [module, command, sub_command, channel])

        # save reference to command handler
        r = re.compile(self.get_regex_from_params(params), re.IGNORECASE | re.DOTALL)
        self.handlers[command_key].append(
            {"regex": r, "callback": handler, "help": help_text, "description": description, "params": params,
             "check_access": check_access})

    def register_command_pre_processor(self, pre_processor):
        """
        Call during start

        Args:
            pre_processor: (context) -> bool
        """

        self.pre_processors.append(pre_processor)

    def register_command_channel(self, label, value):
        """
        Call during pre_start

        Args:
            label: str
            value: str
        """

        if value in self.channels:
            self.logger.error(f"Could not register command channel '{value}': command channel already registered")
            return

        self.logger.debug(f"Registering command channel '{value}'")
        self.channels[value] = label

    def is_command_channel(self, channel):
        return channel in self.channels

    def process_command(self, message: str, channel: str, char_id, reply, conn, followup=True):
        try:
            context = DictObject({"message": message, "char_id": char_id, "channel": channel, "reply": reply})
            for pre_processor in self.pre_processors:
                if pre_processor(context) is False:
                    return

            for regex in self.ignore_regexes:
                if regex.search(message):
                    self.logger.info(f"Ignoring message from {char_id}: {message}")
                    return
            if not followup:
                if channel == "msg":
                    if message.lower().startswith("mail"):
                        self.relay_hub_service.send_message("tell_logger", DictObject({'char_id': char_id, 'name': self.character_service.resolve_char_to_name(char_id)}), f"[FROM] {self.character_service.resolve_char_to_name(char_id)}: mail |usage hidden|",
                                                            f"[FROM] {self.character_service.resolve_char_to_name(char_id)}: mail |usage hidden|")
                    else:
                        self.relay_hub_service.send_message("tell_logger", DictObject({'char_id': char_id, 'name': self.character_service.resolve_char_to_name(char_id)}), f"[FROM] {self.character_service.resolve_char_to_name(char_id)}: {message}", f"[FROM] {self.character_service.resolve_char_to_name(char_id)}: {message}")
            # message = html.unescape(message)
            command_str, command_args = self.get_command_parts(message)

            # check for command alias
            command_alias = self.command_alias_service.check_for_alias(command_str)

            alias_depth_count = 0
            while command_alias:
                alias_depth_count += 1
                command_str, command_args = self.get_command_parts(command_alias + command_args)
                command_alias = self.command_alias_service.check_for_alias(command_str)

                if alias_depth_count > 20:
                    raise Exception(f"Command alias infinite recursion detected for command '{message}'")

            cmd_configs = self.get_command_configs(command_str, channel)
            access_level = self.access_service.get_access_level(char_id)
            sender = SenderObj(char_id, self.character_service.resolve_char_to_name(char_id, f"Unknown({char_id:d})"),
                               access_level)
            if cmd_configs:
                # given a list of cmd_configs that are enabled, see if one has regex that matches incoming command_str
                cmd_config, matches, handler = self.get_matches(cmd_configs, command_args)
                if matches:
                    if handler["check_access"](char_id, cmd_config.access_level):
                        try:
                            response = handler["callback"](CommandRequest(conn, channel, sender, reply),
                                                           *self.process_matches(matches, handler["params"]))
                            if response is not None:
                                if len(response) > 2000 and conn.id == "main" and channel == "msg":
                                    self.bot.send_mass_message(sender.char_id, response)
                                else:
                                    reply(response)

                        except Exception as e:
                            self.logger.error(f"error processing command: {message}", e)
                            self.relay_hub_service.send_message("access_denied_logger", sender,
                                                                f"[ERROR] {sender.name}: {message}",
                                                                f"[ERROR] {sender.name}:  {message}")
                            if channel == "discord":
                                reply(self.getresp("global", "error_processing"))
                            else:
                                self.bot.send_mass_message(char_id, self.getresp("global", "error_processing"))

                        # record command usage
                        self.usage_service.add_usage(command_str, handler["callback"].__qualname__, char_id, channel)
                    else:
                        self.access_denied_response(message, sender, cmd_config, reply, channel)
                else:
                    # handlers were found, but no handler regex matched
                    help_text = self.get_help_text(char_id, command_str, channel)
                    if help_text:
                        reply(self.format_help_text(command_str, help_text))
                    else:
                        # the command is known, but no help is returned, therefore user does not have access to command
                        self.access_denied_response(message, sender, cmd_config, reply, channel)
                        # if access_level['label'] != "all":
                        #     self.relay_hub_service.send_message("access_denied_logger", sender,
                        #                                         f"[DENIED] {sender.name}: {message}",
                        #                                         f"[DENIED] {sender.name}:  {message}")
                        #     self.bot.send_mass_message(char_id, self.getresp("global", "access_denied"))
            else:
                self.handle_unknown_command(command_str, command_args, channel, sender, reply)
        except Exception as e:
            self.logger.error(f"error processing command: {message}", e)
            sender = SenderObj(char_id, self.character_service.resolve_char_to_name(char_id, f"Unknown({char_id:d})"),
                               0)
            self.relay_hub_service.send_message("access_denied_logger", sender, f"[ERROR] {sender.name}: {message}",
                                                f"[ERROR] {sender.name}:  {message}")
            if channel == "discord":
                reply(self.getresp("global", "error_processing"))
            else:
                self.bot.send_mass_message(char_id, self.getresp("global", "error_processing"))

    def handle_unknown_command(self, command_str, command_args, channel, sender, reply):
        self.relay_hub_service.send_message("access_denied_logger", sender,
                                            f"[UNKNOWN] {sender.name}: {command_str} {command_args}",
                                            f"[UNKNOWN] {sender.name}: {command_str} {command_args}")
        if sender.access_level["label"] != "all":
            if channel == "discord":
                self.getresp("global", "unknown_command", {"cmd": command_str})
            else:
                self.bot.send_mass_message(sender.char_id, self.getresp("global", "unknown_command", {"cmd": command_str}))

    def access_denied_response(self, message, sender, cmd_config, reply, channel):
        self.relay_hub_service.send_message("access_denied_logger", sender, f"[DENIED] {sender.name}: {message}",
                                            f"[DENIED] {sender.name}:  {message}")
        if sender.access_level["label"] != "all":
            if channel == "discord":
                reply(self.getresp("global", "access_denied"))
            else:
                self.bot.send_mass_message(sender.char_id, self.getresp("global", "access_denied"))

    def get_command_parts(self, message):
        parts = message.split(" ", 1)
        if len(parts) == 2:
            return parts[0].lower(), " " + parts[1]
        else:
            return parts[0].lower(), ""

    def get_command_configs(self, command, channel=None, enabled=1, sub_command=None):
        sql = "SELECT command, sub_command, access_level, channel, enabled FROM command_config WHERE command = ?"
        params = [command]
        if channel:
            sql += " AND channel = ?"
            params.append(channel)
        if enabled:
            sql += " AND enabled = ?"
            params.append(enabled)
        if sub_command:
            sql += " AND sub_command = ?"
            params.append(sub_command)

        sql += " ORDER BY sub_command, channel"

        return self.db.query(sql, params)

    def get_matches(self, cmd_configs, command_args):
        for row in cmd_configs:
            command_key = self.get_command_key(row.command, row.sub_command)
            handlers = self.handlers[command_key]
            for handler in handlers:
                # add leading space to search string to normalize input for command params
                matches = handler["regex"].search(command_args)
                if matches:
                    return row, matches, handler
        return None, None, None

    def process_matches(self, matches, params):
        groups = list(matches.groups())

        processed = []
        for param in params:
            processed.append(param.process_matches(groups))
        return processed

    def get_help_text(self, char, command_str, channel, show_regex=False):
        data = self.db.query("SELECT command, sub_command, access_level FROM command_config "
                             "WHERE command = ? AND channel = ? AND enabled = 1",
                             [command_str, channel])

        # filter out commands that character does not have access level for
        data = filter(lambda row: self.access_service.check_access(char, row.access_level), data)

        def get_regex(params):
            if show_regex:
                return "\n" + self.get_regex_from_params(params)
            else:
                return ""

        def read_help_text(row):
            command_key = self.get_command_key(row.command, row.sub_command)
            return filter(lambda x: x is not None, map(lambda handler: handler["help"] + get_regex(handler["params"]),
                                                       self.handlers[command_key]))

        content = "\n\n".join(flatmap(read_help_text, data))
        return content if content else None

    def format_help_text(self, topic, help_text):
        return ChatBlob("Help (" + topic + ")", help_text)

    def get_help_file(self, module, help_file):
        if help_file:
            try:
                help_file = "./" + module.replace(".", "/") + "/" + help_file
                with open(help_file, mode="r", encoding="UTF-8") as f:
                    return f.read().strip()
            except FileNotFoundError as e:
                self.logger.error("Error reading help file", e)
        return None

    def get_command_key(self, command, sub_command):
        if sub_command:
            return command + ":" + sub_command
        else:
            return command

    def get_command_key_parts(self, command_str):
        parts = command_str.split(":", 1)
        if len(parts) == 2:
            return parts[0], parts[1]
        else:
            return parts[0], ""

    def get_regex_from_params(self, params):
        # params must be wrapped with line-beginning and line-ending anchors in order to match
        # when no params are specified (eg. "^$")
        return "^" + "".join(map(lambda x: x.get_regex(), params)) + "$"

    def generate_help(self, command, description, params, extended_description=None):
        help_text = description + ":\n" + "<tab><symbol>" + command + " " + " ".join(
            map(lambda x: x.get_name(), params))
        if extended_description:
            help_text += "\n" + extended_description

        return help_text

    def get_handlers(self, command_key):
        return self.handlers.get(command_key, None)

    def handle_private_message(self, conn: Conn, packet: server_packets.PrivateMessage):
        if not self.bot.is_ready():
            if packet.char_id not in self.ignore:
                self.ignore.append(packet.char_id)
                self.bot.send_mass_message(packet.char_id,
                                           "Your command has not been proceeded, bot is still starting.")
            return
        if not self.setting_service.get("accept_commands_from_slave_bots").get_value() and conn.id != "main":
            return

        # since the command symbol is not required for private messages,
        # the command_str must have length of at least 1 in order to be valid,
        # otherwise it is ignored
        if len(packet.message) < 1:
            return

        # ignore leading space
        message = packet.message.lstrip()

        def i():
            self.process_command(
                self.trim_command_symbol(message),
                self.PRIVATE_MESSAGE_CHANNEL,
                packet.char_id,
                lambda msg: self.bot.send_private_message(packet.char_id, msg, conn_id=conn.id),
                conn,
                False)

        t: Thread = Thread(target=i, daemon=True)
        t.start()

    def handle_private_channel_message(self, conn: Conn, packet: server_packets.PrivateChannelMessage):
        if not self.setting_service.get("accept_commands_from_slave_bots").get_value() and conn.id != "main":
            return

        # since the command symbol is required in the private channel,
        # the command_str must have length of at least 2 in order to be valid,
        # otherwise it is ignored
        if len(packet.message) < 2:
            return

        # ignore leading space
        message = packet.message.lstrip()
        if message.startswith(self.setting_service.get("symbol").get_value()) \
                and packet.private_channel_id == self.bot.get_char_id():
            Thread(target=self.process_command(
                self.trim_command_symbol(message),
                self.PRIVATE_CHANNEL,
                packet.char_id,
                lambda msg: self.bot.send_private_channel_message(msg, private_channel_id=conn.char_id,
                                                                  conn_id=conn.id),
                conn,
                False), daemon=True).start()

    def handle_public_channel_message(self, conn: Conn, packet: server_packets.PublicChannelMessage):
        if not self.setting_service.get("accept_commands_from_slave_bots").get_value() and conn.id != "main":
            return

        # since the command symbol is required in the org channel,
        # the command_str must have length of at least 2 in order to be valid,
        # otherwise it is ignored
        if len(packet.message) < 2:
            return

        # ignore leading space
        message = packet.message.lstrip()

        if message.startswith(self.setting_service.get("symbol").get_value()) \
                and self.public_channel_service.is_org_channel_id(packet.channel_id):
            Thread(target=self.process_command(
                self.trim_command_symbol(message),
                self.ORG_CHANNEL,
                packet.char_id,
                lambda msg: self.bot.send_org_message(msg, conn_id=conn.id),
                conn,
                False), daemon=True).start()

    def trim_command_symbol(self, s):
        symbol = self.setting_service.get("symbol").get_value()
        if s.startswith(symbol):
            s = s[len(symbol):]
        return s
