import re
from http import HTTPStatus
from packaging import version
from abc import ABC, abstractmethod
from glpwnme.exploits.exploit import GlpiExploit
from glpwnme.exploits.logger import Log

class PluginExploit(GlpiExploit):
    """
    This class implements the base class
    for a glpi exploit
    """
    _plugin_endpoints = ("marketplace", "plugins")

    def __init__(self, glpi_session):
        super().__init__(glpi_session)
        self.endpoint = None
        self.plugin_exists = None
        self.glpi_plugin_version = None

    @staticmethod
    def extract_version_from_xml(plugin_xml):
        """
        Extract the version of a plugin from its xml file

        :param plugin_xml: The plugin xml recovered
        :type plugin_xml: class:`Response`

        :return: The version found from the plugin file
        :rtype: str
        """
        plugin_xml = plugin_xml.text
        version_plugin = re.search(r'<num>((\d|\.)+?)</num>', plugin_xml)
        if version_plugin:
            return version_plugin.group(1)
        return None

    def _plugins_exists(self, endpoint):
        """
        Check if the plugin exists given an endpoint, set the attribute to know that
        the plugin actually exists, and returns it at every other calls

        :return: True if the plugin exists, False otherwise
        :rtype: bool
        :meta: public
        """
        raise NotImplementedError(f"{self.__class__.__name__}::_plugins_exists not implemented")

    def exists(self):
        """
        Check that the plugin exists
        """
        if self.plugin_exists is not None:
            return self.plugin_exists

        for endp in self.__class__._plugin_endpoints:
            if self._plugins_exists(endp):
                self.endpoint = endp
                self.plugin_exists = True
                break

        if self.plugin_exists is None:
            self.plugin_exists = False
        return self.plugin_exists

    def _get_version_from_xml(self, xml_file=None):
        """
        Recover the version of a plugin from its .xml file

        :return: The version found from the plugin file
        :rtype: str
        """
        if not xml_file:
            xml_file = getattr(self.__class__, "plugin_xml_file", "plugin.xml")

        res = self.plugin_get(xml_file)
        if res.status_code == HTTPStatus.FORBIDDEN:
            self.glpi_session.glpi_infos.is_config_safe = True
            return None

        self.glpi_session.glpi_infos.is_config_safe = False
        return self.__class__.extract_version_from_xml(res)

    def _is_plugin_vulnerable(self, plugin_version):
        """
        Check if the plugin is vulnerable
        """
        plugin_version = version.parse(plugin_version)
        for min_version, max_version in zip(self.requirements.plugin_version.min_version,
                                            self.requirements.plugin_version.max_version):
            min_version = version.parse(min_version)
            max_version = version.parse(max_version)
            if plugin_version >= min_version and plugin_version < max_version:
                return True
        return False

    def requirement_check(self):
        """
        Check all the necessary requirement for the exploit

        :rtype: bool
        """
        if not self.exists():
            Log.err(f"Plugin {self.__class__.plugin_name} not found")
            return False

        if super().requirement_check():
            self.glpi_plugin_version = self.get_plugin_version()
            if(self.requirements.plugin_version is not None and not self.requirements.plugin_version.is_default()):
                if(self.glpi_plugin_version and not self._is_plugin_vulnerable(self.glpi_plugin_version)):
                    Log.err(f"Plugin version is not vulnerable")
                    return False
            return True
        return False

    def are_requirements_checked(self):
        """
        Check all the necessary requirement for the exploit
        but without any print, call only if the class has no check method

        :rtype: bool
        """
        if not self.exists():
            return False

        if super().are_requirements_checked():
            self.glpi_plugin_version = self.get_plugin_version()
            if self.glpi_plugin_version is not None:
                if not self._is_plugin_vulnerable(self.glpi_plugin_version):
                    return False
            else:
                return True
        return False

    def plugin_get(self, url, **kwargs):
        """
        Shortcut for get method on plugin endpoint
        """
        if not self.endpoint:
            raise ValueError("No endpoint set for using PluginExploit::plugin_get method")
        plugin_url = f"/{self.endpoint}/{self.__class__.plugin_name}" + "/" + url.lstrip("/")
        return self.get(plugin_url, **kwargs)

    def plugin_post(self, url, **kwargs):
        """
        Shortcut for post method on plugin endpoint
        """
        if not self.endpoint:
            raise ValueError("No endpoint set for using PluginExploit::plugin_post method")
        plugin_url = f"/{self.endpoint}/{self.__class__.plugin_name}" + "/" + url.lstrip("/")
        return self.post(plugin_url, **kwargs)

    def get_plugin_version(self):
        """
        Function that retrieve the version of the plugin
        By default this function return the version of plugin_name.xml
        """
        return self._get_version_from_xml()

    def check(self):
        """
        Default check method for a plugin exploit
        The check method will also set the endpoint attr of the object

        If the plugin version is not found, it will be considered as vulnerable
        """
        if not self.exists():
            return False

        if self.requirements.plugin_version.is_default():
            return True

        self.glpi_plugin_version = self.get_plugin_version()
        if not self.glpi_plugin_version:
            Log.log("Cannot found the version of the [b]existing[/b] "
                    f"plugin: [b]{self.__class__.plugin_name}[/b]")
            return True

        Log.log(f"Plugin {self.__class__.plugin_name} version [gold3]{self.glpi_plugin_version}[/] found")
        if self._is_plugin_vulnerable(self.glpi_plugin_version):
            return True
        return False
