import requests
from packaging import version
from abc import ABC, abstractmethod
from rich.table import Table
from glpwnme.exploits.logger import Log
from .requirements import VersionRequirement, DefaultVersion, Requirements
from .metadatas import Metadata
from .privileges import Privs
from .utils import GlpiUtils, LoginResult
from .exceptions import ExploitNotOpsecException

class GlpiExploit(ABC):
    """
    This class implements the base class
    for a glpi exploit
    """
    def __init__(self, glpi_session):
        self.glpi_session = glpi_session
        self.__init_metadatas()
        self.__init_requirements()
        self.__init_privileges()
        self.__init_impact()

    def _on_before_check(self):
        """
        Before any check, login to the target instance if needed, and
        verify that the privileges are those required

        :raises ValueError: If no rcedentials has been provided but required
            or if login failed

        :return: Whether or not everything is set up
        :rtype: bool

        TODO: check privileges of the user when logged in
        """
        if self.privilege.value == 0:
            return True

        login_status = self.glpi_session.login_with_credentials()
        if login_status:
            return True

        raise ValueError("Login failed")

    def _on_after_check(self):
        """
        Once the check has been done, logout
        """
        if self.privilege.value == 0:
            return True

        self.glpi_session.logout()

    def _write_log(self, msg):
        """
        Wrapper around the Log.write method

        :param msg: The message to log
        :type msg: Union[str, bytes]
        """
        if isinstance(msg, bytes):
            msg = msg.decode()

        cls_name = self.__class__.__name__
        Log.write(f"{cls_name}: {msg}")

    def start_check_workflow(self, opsec_only=True):
        """
        Start the workflow to run the
        exploit

        :param opsec_only: Whether or not to check an exploit that is not
            opsec safe
        :type opsec_only: bool

        :raises: An Exception got from the internal function

        :return: Whether or not the workflow succeed
        :rtype: bool
        """
        local_exception = None
        is_check_opsec_safe = getattr(self, "_is_check_opsec_safe", True)
        if(opsec_only and not is_check_opsec_safe):
            raise ExploitNotOpsecException("Check is not opsec safe, use --no-opsec")

        try:
            self._on_before_check()
            check_result = self.check()

        except Exception as e:
            local_exception = e
            check_result = False

        finally:
            self._on_after_check()

        if local_exception:
            raise local_exception
        return check_result

    def start_run_workflow(self, **options):
        """
        Start the workflow to run the
        exploit, will just login the user if needed.
        No logout will be proceed, as it could remove sessions cookies
        """
        self._on_before_check()
        self.run(**options)

    def start_clean_workflow(self, **options):
        """
        Start the workflow to clean the
        exploit, will just login the user if needed.
        No logout will be proceed, as it could remove sessions cookies
        """
        self._on_before_check()
        self.clean(**options)

    def _get_version_attr(self, version_name):
        """
        Recover the version from the class and returns it
        """
        min_version_name = "min_" + version_name
        max_version_name = "max_" + version_name
        if(hasattr(self, min_version_name) or hasattr(self, max_version_name)):
            min_version = getattr(self, min_version_name, DefaultVersion.minimal)
            max_version = getattr(self, max_version_name, DefaultVersion.maximal)
            if isinstance(min_version, str):
                min_version = (min_version, )
            if isinstance(max_version, str):
                max_version = (max_version, )
            if len(min_version) != len(max_version):
                raise ValueError("Versions must have the same number of parameter: "
                                 f"len({min_version}) != len({max_version})")
            return VersionRequirement(min_version=min_version,
                                      max_version=max_version)
        return VersionRequirement()

    def __init_requirements(self):
        """
        Init the requirements of the exploit if any
        By default the exploit is compatible with everything
        """
        glpi_version = self._get_version_attr("version")
        php_version = self._get_version_attr("php_version")
        plugin_version = self._get_version_attr("plugin_version")
        operating_system = getattr(self, "operating_system", "All").capitalize()
        api_required = getattr(self, "require_api", False)
        require_inventory = getattr(self, "require_inventory", False)
        self.requirements = Requirements(glpi_version=glpi_version,
                                         php_version=php_version,
                                         plugin_version=plugin_version,
                                         os_used=operating_system,
                                         api_status=api_required,
                                         require_inventory=require_inventory)

    def __init_metadatas(self):
        """
        Init the meta data of the exploit, such as:
         - author
         - CVSS score
         - description
        """
        self.metadatas = Metadata.parse_from_comment(self.__doc__)
        description = getattr(self, "_description", "")
        if not self.metadatas:
            author = getattr(self, "_author", "").title()
            name = getattr(self, "_name", "")
            cvss = float(getattr(self, "_score", 0))
            self.metadatas = Metadata(author=author,
                                      description=description,
                                      name=name,
                                      cvss=cvss)
        elif description:
            self.metadatas.description = description

    def __init_privileges(self):
        """
        Init the privileges required for the exploit
        """
        try:
            privilege_name = getattr(self, "_privilege", "Unauthenticated").capitalize().replace('-','')
            self.privilege = Privs[privilege_name]
        except KeyError:
            privs_available = Privs.list()
            unknown = getattr(self, "_privilege", "Unauthenticated").capitalize()
            Log.print(f"Error in class [bold]{self.__class__.__name__}[/bold]")
            Log.print(f"Privilege [red]{unknown}[/red] unknown, please use one of the following:")
            Log.print(" - " + "\n - ".join(privs_available))
            exit(0)

    def __init_impact(self):
        """
        Init the impacts of the exploit
        """
        try:
            self.impacts = list(map(lambda x: x.strip().title(), getattr(self, "_impacts").split(",")))
        except AttributeError:
            Log.print(f"[red]Error in class [bold]{self.__class__.__name__}[/bold][/red]")
            Log.print("_impacts is required to work correctly")
            Log.print("Example: _impacts = 'Authentication Bypass'")
            exit(0)

    def is_glpi_vulnerable(self, glpi_version):
        """
        Check if the exploit is usable given a version of glpi

        .. code-block::
            class MyExploit(GlpiExploit):
                min_version = ("9.5.0", "10.0.0")
                max_version = ("9.5.7", "10.0.21")

            my_exploit = MyExploit(...)

            my_exploit.is_glpi_vulnerable("10.0.18") # Return True
            my_exploit.is_glpi_vulnerable("10.0.22") # Return False
        """
        glpi_version = version.parse(glpi_version)
        for min_version, max_version in zip(self.requirements.glpi_version.min_version,
                                            self.requirements.glpi_version.max_version):
            min_version = version.parse(min_version)
            max_version = version.parse(max_version)
            if glpi_version >= min_version and glpi_version < max_version:
                return True
        return False

    def is_version_equal_or_above(self, target_version):
        """
        Check if version is above or equald another version
        """
        checked_version = self.glpi_session.glpi_infos.glpi_version
        if checked_version is None:
            checked_version = self.glpi_session.glpi_infos.glpi_nearest_version
            if checked_version is None:
                Log.err(f"No version found...")
                return False
        min_version = version.parse(checked_version)
        return min_version >= version.parse(target_version)

    def is_php_vulnerable(self, php_version):
        """
        Check if the exploit is usable given a version of php
        """
        php_version = version.parse(php_version)
        for min_version, max_version in zip(self.requirements.php_version.min_version,
                                            self.requirements.php_version.max_version):
            min_version = version.parse(min_version)
            max_version = version.parse(max_version)
            if php_version >= min_version and php_version < max_version:
                return True
        return False

    def is_os_compatible(self, os_in_use=None):
        """
        Check if the remote operating system is compatible with the exploit
        """
        if self.requirements.os_used == "All":
            return True
        return self.requirements.os_used == os_in_use.capitalize()

    def requirement_check(self):
        """
        Check all the necessary requirement for the exploit
        
        :rtype: bool
        """
        glpi_version = self.glpi_session.glpi_infos.glpi_version
        classname = self.__class__.__name__
        if not self.requirements.glpi_version.is_default():
            if glpi_version is not None:
                if not self.is_glpi_vulnerable(glpi_version):
                    Log.err(f"[red]{classname} Glpi version[/red] is not vulnerable")
                    return False
            else:
                Log.err(f"[red]Cannot recover glpi version, "
                   "continuing exploit anyway...[/red]")

        php_version = self.glpi_session.glpi_infos.php_version
        if not self.requirements.php_version.is_default():
            if php_version is not None:
                if not self.is_php_vulnerable(php_version):
                    Log.err(f"[red]{classname} Php version[/red] is not vulnerable")
                    return False
            else:
                Log.err(f"[red]Cannot recover php version, "
                   "continuing exploit anyway...[/red]")

        operating_system_server = self.glpi_session.glpi_infos.os_used
        if not self.is_os_compatible(operating_system_server):
            Log.err(f"[red]{classname} Os family[/red] is not vulnerable")
            return False

        api_status = self.glpi_session.glpi_infos.api_status
        if(self.requirements.api_status and not api_status):
            Log.err(f"[red]{classname} Api[/red] is not enabled")
            return False

        inventory_status = self.glpi_session.glpi_infos.inventory_status
        if(self.requirements.require_inventory and not inventory_status):
            Log.err(f"[red]{classname} Inventory[/red] is not enabled")
            return False
        return True

    def are_requirements_checked(self):
        """
        Check all the necessary requirement for the exploit
        but without any print
        
        :rtype: bool
        """
        glpi_version = self.glpi_session.glpi_infos.glpi_version
        classname = self.__class__.__name__
        if not self.requirements.glpi_version.is_default():
            if glpi_version is not None:
                if not self.is_glpi_vulnerable(glpi_version):
                    return False

        php_version = self.glpi_session.glpi_infos.php_version
        if not self.requirements.php_version.is_default():
            if php_version is not None:
                if not self.is_php_vulnerable(php_version):
                    return False

        operating_system_server = self.glpi_session.glpi_infos.os_used
        if not self.is_os_compatible(operating_system_server):
            return False

        api_status = self.glpi_session.glpi_infos.api_status
        if(self.requirements.api_status and not api_status):
            return False

        inventory_status = self.glpi_session.glpi_infos.inventory_status
        if(self.requirements.require_inventory and not inventory_status):
            return False
        return True

    def display_short(self):
        """
        Display the informations of the exploit in short
        """
        short = f"[bold]Exploit [blue]{self.metadatas.name}[/blue]:[/bold] "
        short += f"Privilege - [yellow bold]{self.privilege.name}[/yellow bold]"
        short += f", Impact - {self.impacts}"
        if self.metadatas.cvss != 0:
            short += f" ([bold]{self.metadatas.cvss}[/bold]/10)"
        Log.print(short)

    def display(self):
        """
        Display the whole details stores in the exploit
        """
        table = GlpiExploit.get_table_header()

        table.add_row(*self.get_row())
        Log.print(table)

    @staticmethod
    def get_table_header(title="Exploit"):
        """
        Return the table header to display an exploit

        :param title: The title of the table
        :type title: str

        :return: The table with its columns set
        :rtype: class:rich.Table
        """
        table = Table(title=title)

        table.add_column("Author", style="red")
        table.add_column("Name", style="cyan")
        table.add_column("Score", style="yellow")
        table.add_column("Impacts")
        table.add_column("Version")
        table.add_column("Privileges", justify="center")
        return table

    def get_row(self):
        """
        Return the row to be display in a table

        :return: The row in a tuple
        :rtype: tuple
        """
        version_banner = ""
        for min_version, max_version in zip(self.requirements.glpi_version.min_version,
                                            self.requirements.glpi_version.max_version):
            version_banner += f"{min_version} <= VERSION < {max_version}\n"
        version_banner = version_banner.replace(DefaultVersion.maximal, "[yellow]\u221E[/yellow]")

        priv = ":skull: " + self.privilege.name if self.privilege.value == 0 else self.privilege.name

        return (self.metadatas.author,
                self.metadatas.name,
                self.metadatas.cvss,
                ", ".join(self.impacts),
                version_banner,
                priv)

    def get(self, url, **kwargs):
        """
        Shortcut for self.glpi_session.get
        """
        return self.glpi_session.get(url, **kwargs)

    def post(self, url, **kwargs):
        """
        Shortcut for self.glpi_session.post
        CSRF token will be automatically updated here
        """
        return self.glpi_session.post(url, **kwargs)

    def clean(self):
        """
        The clean method is not mandatory for an exploit
        and will just log a message on the screen by default
        """
        Log.log("No [blue]clean[/blue] method implemented for this exploit")

