import inspect
from glpwnme.exploits.logger import Log
from .exceptions import ExploitNotOpsecException

class ExploitOrchestrator:
    """
    This class is the orchestrator of the exploits
    available
    It will be used to call the right function with the right arguments
    depending of the user input
    """
    def __init__(self, exploits):
        self.exploits = exploits

    @staticmethod
    def display_exploits(exploits):
        """
        List all the exploits in a table

        :param exploits: The list of exploits class
        :type exploits: List[class:`exploits.GlpiExploit`]
        """
        if not exploits:
            return

        table = exploits[0].get_table_header("Exploit List")
        table.add_column("Options", style="blue")
        for exp_class in exploits:
            params = ExploitOrchestrator.get_function_params(exp_class)
            params_display = ",".join(params["required"]) + ",".join(params["optional"])
            table.add_row(*exp_class.get_row(), params_display)
        Log.print(table)

    @staticmethod
    def get_function_params(exploit, func_name="run"):
        """
        Return the functions parameters of an exploit
        
        :param exploit: The exploit on which to look
        :type exploit: class:`GlpiExploit`

        :param func_name: The name of the function to check
        :type func_name: str
        """
        parameters = {"required": [], "optional": []}
        if not hasattr(exploit, func_name):
            return parameters

        signature = inspect.signature(getattr(exploit, func_name))
        for param_name, default in signature.parameters.items():
            if default.default is not default.empty:
                # Parameters is optional, default.annotation shall return the type if given
                # default.annotation --> <class 'inspect._empty'> | <class 'int'> | ...
                parameters["optional"].append(param_name)
            else:
                parameters["required"].append(param_name)
        return parameters

    @staticmethod
    def is_missing_param(params, options=None):
        """
        Check if params are missing from the options
        given by the user

        :param params: The dictionnary containing the required and optionnal params
        :type params: Dict[str, List[str]]

        :param options: The dict containing the options given by the user
        :type options: Dict[str, str]

        :return: Whether or not a param is missing
        :rtype: bool
        """
        if options is None:
            options = {}

        for required in params["required"]:
            if required not in options.keys():
                return True
        return False

    @staticmethod
    def build_usage(params):
        """
        Build the options required for the exploit

        :return: The usage of the command
        :rtype: str
        """
        usage = f"-O "
        for param in params["required"]:
            usage += f'"{param}=<value>" '

        for param in params["optional"]:
            usage += f'"{param}=<?value>" '
        return usage.strip()

    def recover_exploit_named(self, name):
        """
        Recover an exploit class from the name given by the user
        This function is case insensitive

        :param name: The name of the exploit to use
        :type name: str

        :return: The exploit to use
        :rtype: class:`exploits.GlpiExploit`
        """
        for exploit in self.exploits:
            classname = exploit.__class__.__name__
            if exploit.metadatas.name != "":
                if(exploit.metadatas.name.lower() == name.lower()
                    or classname.lower() == name.lower()):
                    return exploit
        return None

    def _run_check(self, exploit, opsec_only=True):
        """
        Run the check function on an exploit and returns its result
        """
        Log.log("Starting check")
        failed_message = "Target seems not vulnerable"
        try:
            if exploit.start_check_workflow(opsec_only):
                Log.msg("Target seems vulnerable")
                return True

        except Exception as e:
            failed_message = f"Got error: {e}"
        Log.err(failed_message)
        return False

    def check_exploit(self, exploit_name, opsec_only=True):
        """
        Check if the target is vulnerable to a specific exploit

        :param exploit_name: The name of the exploit (Case insensitive)
        :type exploit: str

        :param force: Force the call to check function
        :type force: bool

        :return: Whether or not the target is vulnerable
        :rtype: bool
        """
        exploit = self.recover_exploit_named(exploit_name)
        if not exploit:
            Log.err(f"Cannot find the exploit named: {exploit_name}")
            return False

        if exploit.requirement_check():
            Log.msg("Requirements seems ok")
            return self._run_check(exploit, opsec_only)

        return False

    @staticmethod
    def _has_checked_method(exploit):
        """
        Check if the exploit has a method named check

        :param exploit: The exploit to check
        :type exploit: class:`GlpiExploit`

        :return: Whether the exploit has a check method or not
        :rtype: bool
        """
        return hasattr(exploit, "check") and callable(getattr(exploit, "check"))

    def get_checked_exploits(self, opsec_only=True):
        """
        Check all exploits against the target and
        return those who validated the check

        :param opsec_only: Whether or not to check the exploit that are not opsec safe
        :type opsec_only: bool
        """
        checked_exploit = []
        for exploit in self.exploits:
            Log.log(f"Checking [red]{exploit.__class__.__name__}[/red] ", end="\r")
            try:
                if not ExploitOrchestrator._has_checked_method(exploit):
                    version_target = exploit.glpi_session.glpi_infos.glpi_version
                    if exploit.are_requirements_checked():
                        Log.print(f":high_voltage: [bright_yellow]{exploit.__class__.__name__}[/bright_yellow]: Version seems vulnerable !",
                                  end="\x1b[K\n")
                        checked_exploit.append(exploit)
                    else:
                        Log.print(f"[red]:x:[/red] [red]{exploit.__class__.__name__}[/red] Version not vulnerable",
                                  end="\x1b[K\n")

                elif exploit.start_check_workflow(opsec_only):
                    checked_exploit.append(exploit)
                    Log.print(f"[green]:white_check_mark:[/green] Checking [b green]{exploit.__class__.__name__}[/b green]",
                              end="\x1b[K\n")
                else:
                    Log.print(f"[red]:x:[/red] Checking [red]{exploit.__class__.__name__}[/red]",
                              end="\x1b[K\n")

            # Those errors might happen if you check exploit that requires credentials
            # but do not provide any
            except (ExploitNotOpsecException, ValueError) as e:
                if exploit.are_requirements_checked():
                    Log.print(f":high_voltage: [bright_yellow]{exploit.__class__.__name__}[/bright_yellow]:"
                              f" [b]{e}[/b], however version seems vulnerable !")
                    checked_exploit.append(exploit)
                else:
                    Log.print(f"[red]:x:[/red] [red]{exploit.__class__.__name__}[/red]: [b]{e}[/b]",
                              end="\x1b[K\n")

            except Exception as e:
                Log.print(f"[red]:x:[/red] [red]{exploit.__class__.__name__}[/red]: [b]{e}[/b]",
                          end="\x1b[K\n")
        return checked_exploit

    def run_exploit(self, exploit_name, options=None):
        """
        Run the exploit on the target

        :param exploit_name: The name of the exploit (Case insensitive)
        :type exploit: str

        :raises ValueError: If options are missing for running the function
        """
        if not options:
            options = {}

        exploit = self.recover_exploit_named(exploit_name)
        if not exploit:
            Log.err(f"Cannot find the exploit named: {exploit_name}")
            return

        params = self.get_function_params(exploit)
        if self.is_missing_param(params, options):
            Log.err(f"Wrong usage, missing options:")
            print("```" + self.build_usage(params) + "```")
            Log.err(f"Exiting...")
            exit(0)

        exploit.start_run_workflow(**options)

    def clean_exploit(self, exploit_name, options=None):
        """
        Clean the exploit's traces on the target

        :param exploit_name: The name of the exploit (Case insensitive)
        :type exploit: str

        :raises ValueError: If options are missing for running the function
        """
        if not options:
            options = {}

        exploit = self.recover_exploit_named(exploit_name)
        if not exploit:
            Log.err(f"Cannot find the exploit named: {exploit_name}")
            return

        params = self.get_function_params(exploit, func_name="clean")
        if self.is_missing_param(params, options):
            Log.err(f"Wrong usage, missing options:")
            print("```" + self.build_usage(params) + "```")
            exit(0)

        exploit.start_clean_workflow(**options)

    def get_header_infos(self, exploit):
        """
        Get some header information about an exploit

        :param exploit: The exploit from which to show infos
        :type exploit: class:`GlpiExploit`

        :return: The infos about the exploit
        :rtype: str
        """
        exploit_name = exploit.__class__.__name__
        exploit_author = exploit.metadatas.author
        exploit_score = exploit.metadatas.cvss
        exploit_privs = exploit.privilege.name

        min_version = exploit.requirements.glpi_version.min_version
        max_version = exploit.requirements.glpi_version.max_version

        header = f"[b]{exploit_name}[/]\nCVSS: {exploit_score}/10\n"
        header += f"Author: [dodger_blue1]{exploit_author}[/]\n"
        header += f"Privileges required: [dodger_blue1]{exploit_privs}[/]\n"
        header += f"[dodger_blue1]Vulnerable[/] from Version {min_version} and strictly below {max_version}\n"
        return header

    def show_infos_about(self, exploit_name):
        """
        Show infos about an exploit

        :param exploit_name: The name of the exploit (Case insensitive)
        :type exploit: str
        """
        exploit = self.recover_exploit_named(exploit_name)
        if not exploit:
            Log.err(f"Cannot find the exploit named: {exploit_name}")
            return

        infos_method = getattr(exploit, "infos", None)
        if callable(infos_method):
            Log.print(self.get_header_infos(exploit))
            Log.print(infos_method())

        else:
            Log.err(f"infos method is not implemented in [red]{exploit_name}[/]")

    def get_potential_exploits(self):
        """
        Return the potential exploit by checking the versions
        """
        potential_exploits = []
        for exploit in self.exploits:
            if exploit.requirement_check():
                potential_exploits.append(exploit)
        return potential_exploits
