#!/usr/bin/env python

# SPDX-FileCopyrightText: the secureCodeBox authors
#
# SPDX-License-Identifier: Apache-2.0

# -*- coding: utf-8 -*-

import collections
import logging

from abc import ABC, abstractmethod
from zapv2 import ZAPv2

from .configuration import ZapConfiguration

# set up logging to file - see previous section for more details
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(name)-12s %(levelname)-8s: %(message)s",
    datefmt="%Y-%m-%d %H:%M",
)

logging = logging.getLogger("ZapClient")


class ZapClient(ABC):
    """This abstract class configures a ZAP Client using in a running ZAP instance."""

    def __init__(self, zap: ZAPv2, config: ZapConfiguration):
        """Initial constructor used for this class

        Parameters
        ----------
        zap : ZAPv2
            The running ZAP instance to configure.
        config : ZapConfiguration
            The configuration object containing all ZAP configs (based on the class ZapConfiguration).
        """

        self.__zap = zap
        self.__config = config

    @property
    def get_config(self) -> ZapConfiguration:
        """Returns the complete config of the currently running ZAP instance."""
        return self.__config

    @property
    def get_zap(self) -> ZAPv2:
        """Returns the currently running ZAP instance."""
        return self.__zap

    def check_zap_result(
        self, result: str, method_name: str, exception_message=None
    ) -> bool:
        """Checks the given result for ZAP API Call for errors and logs a warning messages if there are errors returened by ZAP.

        Parameters
        ----------
        result: str
            The result of a ZAP API Call.
        method_name: str
            The name of the method used (to call ZAP) used to log a warning, if the given result is not "OK".
        exception_message: str
            The exception message that must be thrown with an Exception, if the given result is not "OK".
        """

        __result = False

        if "OK" != result:
            __result = False
            if exception_message is not None:
                logging.error(exception_message)
                raise Exception(exception_message)
            else:
                logging.warning(
                    "Failed to call ZAP Method ['%s'], result is: '%s'",
                    method_name,
                    result,
                )
        else:
            logging.debug(
                "Successful called ZAP Method ['%s'], result is: '%s'",
                method_name,
                result,
            )
            __result = True

        return __result

    def configure_scripts(self, config: collections.OrderedDict):
        """Private method to configure the script settings, based on the configuration settings."""

        if self._is_not_empty("scripts", config):
            self._log_all_scripts()
            for script in config["scripts"]:
                logging.debug("Configuring Script: '%s'", script["name"])
                self._configure_load_script(script_config=script, script_type=None)
            self._log_all_scripts()
        else:
            logging.debug("No Scripts found to configure.")

    def _configure_load_script(
        self, script_config: collections.OrderedDict, script_type: str
    ):
        """Protected method to load a new ZAP Script based on a given ZAP config.

        Parameters
        ----------
        script_config : collections.OrderedDict
            The current 'script'  configuration object containing the ZAP script configuration (based on the class ZapConfiguration).
        """

        if self._is_not_empty("name", script_config):

            # Set default to script_type if it is defined
            if (
                script_type is not None
                and isinstance(script_type, str)
                and len(script_type) > 0
            ):
                script_config["type"] = script_type

            # Only try to add new scripts if the definition contains all nessesary config options, otherwise try to only activate/deactivate a given script name
            if (
                "filePath" in script_config
                and "engine" in script_config
                and "type" in script_config
            ):
                # Remove existing Script, if already pre-existing
                logging.debug(
                    "Trying to remove pre-existing Script '%s' at '%s'",
                    script_config["name"],
                    script_config["filePath"],
                )
                self.get_zap.script.remove(scriptname=script_config["name"])

                # Add Script again
                logging.info(
                    "Loading new Script '%s' at '%s' with type: '%s' and engine '%s'",
                    script_config["name"],
                    script_config["filePath"],
                    script_config["type"],
                    script_config["engine"],
                )
                self.check_zap_result(
                    result=self.get_zap.script.load(
                        scriptname=script_config["name"],
                        scripttype=script_config["type"],
                        scriptengine=script_config["engine"],
                        filename=script_config["filePath"],
                        scriptdescription=script_config["description"],
                    ),
                    method_name="script.load",
                    exception_message="The script couldn't be loaded due to errors!",
                )

            # Set default to: True
            if not self._is_not_empty("enabled", script_config):
                script_config["enabled"] = True

            logging.info(
                "Activating Script '%s' with 'enabled: %s'",
                script_config["name"],
                str(script_config["enabled"]).lower(),
            )
            if script_config["enabled"]:
                self.check_zap_result(
                    result=self.get_zap.script.enable(scriptname=script_config["name"]),
                    method_name="script.enable",
                )
            else:
                self.check_zap_result(
                    result=self.get_zap.script.disable(
                        scriptname=script_config["name"]
                    ),
                    method_name="script.disable",
                )
        else:
            logging.warning(
                "Important script configs (name, type, filePath, engine) are missing! Ignoring the script configuration. Please check your YAML configuration."
            )

    def _log_all_scripts(self):
        """Protected method to log all currently configured ZAP Scripts."""

        for scripts in self.get_zap.script.list_scripts:
            logging.debug(scripts)

    def _is_not_empty(self, item_name: str, config: collections.OrderedDict) -> bool:
        """Return True if the item with the name 'item_name' is exisiting and not None, otherwise false."""
        result = False
        if (
            config is not None
            and item_name in config
            and (config[item_name] is not None)
        ):
            result = True
        return result

    def _is_not_empty_integer(
        self, item_name: str, config: collections.OrderedDict
    ) -> bool:
        """Return True if the item with the name 'item_name' is exisiting and a valid integer >= 0, otherwise false."""
        result = False
        if (
            self._is_not_empty(item_name, config)
            and isinstance(config[item_name], int)
            and config[item_name] >= 0
        ):
            result = True
        return result

    def _is_not_empty_string(
        self, item_name: str, config: collections.OrderedDict
    ) -> bool:
        """Return True if the item with the name 'item_name' is exisiting and a valid string with len() >= 0, otherwise false."""
        result = False
        if (
            self._is_not_empty(item_name, config)
            and isinstance(config[item_name], str)
            and len(config[item_name]) > 0
        ):
            result = True
        return result

    def _is_not_empty_bool(
        self, item_name: str, config: collections.OrderedDict
    ) -> bool:
        """Return True if the item with the name 'item_name' is exisiting and a valid bool, otherwise false."""
        result = False
        if self._is_not_empty(item_name, config) and isinstance(
            config[item_name], bool
        ):
            result = True
        return result
