# Copyright (c) 2019 Cisco and / or its affiliates.
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy
# of the License at:
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions
# and limitations under the License.

"""wrk traffic profile parser.

See LLD for the structure of a wrk traffic profile.
"""


from os.path import isfile
from pprint import pformat

from yaml import safe_load, YAMLError
from robot.api import logger

from resources.tools.wrk.wrk_errors import WrkError


class WrkTrafficProfile:
    """The wrk traffic profile.
    """

    MANDATORY_PARAMS = (
        u"urls",
        u"first-cpu",
        u"cpus",
        u"duration",
        u"nr-of-threads",
        u"nr-of-connections"
    )

    INTEGER_PARAMS = (
        (u"cpus", 1),
        (u"first-cpu", 0),
        (u"duration", 1),
        (u"nr-of-threads", 1),
        (u"nr-of-connections", 1)
    )

    def __init__(self, profile_name):
        """Read the traffic profile from the yaml file.

        :param profile_name: Path to the yaml file with the profile.
        :type profile_name: str
        :raises: WrkError if it is not possible to parse the profile.
        """

        self._profile_name = None
        self._traffic_profile = None

        self.profile_name = profile_name

        try:
            with open(self.profile_name, u"rt") as profile_file:
                self.traffic_profile = safe_load(profile_file)
        except IOError as err:
            raise WrkError(
                msg=f"An error occurred while opening the file "
                f"'{self.profile_name}'.", details=str(err)
            )
        except YAMLError as err:
            raise WrkError(
                msg=f"An error occurred while parsing the traffic profile "
                f"'{self.profile_name}'.", details=str(err)
            )

        self._validate_traffic_profile()

        if self.traffic_profile:
            logger.debug(
                f"\nThe wrk traffic profile '{self.profile_name}' is valid.\n"
            )
            logger.debug(f"wrk traffic profile '{self.profile_name}':")
            logger.debug(pformat(self.traffic_profile))
        else:
            logger.debug(
                f"\nThe wrk traffic profile '{self.profile_name}' is invalid.\n"
            )
            raise WrkError(
                f"\nThe wrk traffic profile '{self.profile_name}' is invalid.\n"
            )

    def __repr__(self):
        return pformat(self.traffic_profile)

    def __str__(self):
        return pformat(self.traffic_profile)

    def _validate_traffic_profile(self):
        """Validate the traffic profile.

        The specification, the structure and the rules are described in
        doc/wrk_lld.rst
        """

        logger.debug(
            f"\nValidating the wrk traffic profile '{self.profile_name}'...\n"
        )
        if not (self._validate_mandatory_structure()
                and self._validate_mandatory_values()
                and self._validate_optional_values()
                and self._validate_dependencies()):
            self.traffic_profile = None

    def _validate_mandatory_structure(self):
        """Validate presence of mandatory parameters in trafic profile dict

        :returns: whether mandatory structure is followed by the profile
        :rtype: bool
        """
        # Level 1: Check if the profile is a dictionary:
        if not isinstance(self.traffic_profile, dict):
            logger.error(u"The wrk traffic profile must be a dictionary.")
            return False

        # Level 2: Check if all mandatory parameters are present:
        is_valid = True
        for param in self.MANDATORY_PARAMS:
            if self.traffic_profile.get(param, None) is None:
                logger.error(f"The parameter '{param}' in mandatory.")
                is_valid = False
        return is_valid

    def _validate_mandatory_values(self):
        """Validate that mandatory profile values satisfy their constraints

        :returns: whether mandatory values are acceptable
        :rtype: bool
        """
        # Level 3: Mandatory params: Check if urls is a list:
        is_valid = True
        if not isinstance(self.traffic_profile[u"urls"], list):
            logger.error(u"The parameter 'urls' must be a list.")
            is_valid = False

        # Level 3: Mandatory params: Check if integers are not below minimum
        for param, minimum in self.INTEGER_PARAMS:
            if not self._validate_int_param(param, minimum):
                is_valid = False
        return is_valid

    def _validate_optional_values(self):
        """Validate values for optional parameters, if present

        :returns: whether present optional values are acceptable
        :rtype: bool
        """
        is_valid = True
        # Level 4: Optional params: Check if script is present:
        script = self.traffic_profile.get(u"script", None)
        if script is not None:
            if not isinstance(script, str):
                logger.error(u"The path to LuaJIT script in invalid")
                is_valid = False
            else:
                if not isfile(script):
                    logger.error(f"The file '{script}' does not exist.")
                    is_valid = False
        else:
            self.traffic_profile[u"script"] = None
            logger.debug(
                u"The optional parameter 'LuaJIT script' is not defined. "
                u"No problem."
            )

        # Level 4: Optional params: Check if header is present:
        header = self.traffic_profile.get(u"header", None)
        if header is not None:
            if isinstance(header, dict):
                header = u", ".join(
                    f"{0}: {1}".format(*item) for item in header.items()
                )
                self.traffic_profile[u"header"] = header
            elif not isinstance(header, str):
                logger.error(u"The parameter 'header' type is not valid.")
                is_valid = False

            if not header:
                logger.error(u"The parameter 'header' is defined but empty.")
                is_valid = False
        else:
            self.traffic_profile[u"header"] = None
            logger.debug(
                u"The optional parameter 'header' is not defined. No problem."
            )

        # Level 4: Optional params: Check if latency is present:
        latency = self.traffic_profile.get(u"latency", None)
        if latency is not None:
            if not isinstance(latency, bool):
                logger.error(u"The parameter 'latency' must be boolean.")
                is_valid = False
        else:
            self.traffic_profile[u"latency"] = False
            logger.debug(
                u"The optional parameter 'latency' is not defined. No problem."
            )

        # Level 4: Optional params: Check if timeout is present:
        if u"timeout" in self.traffic_profile:
            if not self._validate_int_param(u"timeout", 1):
                is_valid = False
        else:
            self.traffic_profile[u"timeout"] = None
            logger.debug(
                u"The optional parameter 'timeout' is not defined. No problem."
            )

        return is_valid

    def _validate_dependencies(self):
        """Validate dependencies between parameters

        :returns: whether dependencies between parameters are acceptable
        :rtype: bool
        """
        # Level 5: Check urls and cpus:
        if self.traffic_profile[u"cpus"] % len(self.traffic_profile[u"urls"]):
            logger.error(
                u"The number of CPUs must be a multiple of the number of URLs."
            )
            return False
        return True

    def _validate_int_param(self, param, minimum):
        """Validate that an int parameter is set acceptably
        If it is not an int already but a string, convert and store it as int.

        :param param: Name of a traffic profile parameter
        :param minimum: The minimum value for the named parameter
        :type param: str
        :type minimum: int
        :returns: whether param is set to an int of at least minimum value
        :rtype: bool
        """
        value = self._traffic_profile[param]
        if isinstance(value, str):
            if value.isdigit():
                value = int(value)
            else:
                value = minimum - 1
        if isinstance(value, int) and value >= minimum:
            self.traffic_profile[param] = value
            return True
        logger.error(
            f"The parameter '{param}' must be an integer and at least {minimum}"
        )
        return False

    @property
    def profile_name(self):
        """Getter - Profile name.

        :returns: The traffic profile file path
        :rtype: str
        """
        return self._profile_name

    @profile_name.setter
    def profile_name(self, profile_name):
        """

        :param profile_name:
        :type profile_name: str
        """
        self._profile_name = profile_name

    @property
    def traffic_profile(self):
        """Getter: Traffic profile.

        :returns: The traffic profile.
        :rtype: dict
        """
        return self._traffic_profile

    @traffic_profile.setter
    def traffic_profile(self, profile):
        """Setter - Traffic profile.

        :param profile: The new traffic profile.
        :type profile: dict
        """
        self._traffic_profile = profile