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

# Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
#
# 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.

import logging
import operator

log = logging.getLogger(__name__)


class RuleException(Exception):
    pass


class KeyNotFoundException(RuleException):
    pass


class UnknownTypeException(RuleException):
    pass


class BadConfigException(RuleException):
    pass


class InvalidValueException(RuleException):
    pass


class RuleBase(object):
    @classmethod
    def default_dict_struct_getter(cls, struct, field_name):
        """
        returns a key from dictionary based on field_name, if the name contains
        `:` then it means additional nesting levels should be checked for the
        key so `a:b:c` means return struct['a']['b']['c']

        :param struct:
        :param field_name:
        :return:
        """
        parts = field_name.split(":") if field_name else []
        found = struct
        while parts:
            current_key = parts.pop(0)
            found = found.get(current_key)
            if not found and parts:
                raise KeyNotFoundException("Key not found in structure")
        return found

    @classmethod
    def default_obj_struct_getter(cls, struct, field_name):
        """
        returns a key from instance based on field_name, if the name contains
        `:` then it means additional nesting levels should be checked for the
        key so `a:b:c` means return struct.a.b.c

        :param struct:
        :param field_name:
        :return:
        """
        parts = field_name.split(":")
        found = struct
        while parts:
            current_key = parts.pop(0)
            found = getattr(found, current_key, None)
            if not found and parts:
                raise KeyNotFoundException("Key not found in structure")
        return found

    def normalized_type(self, field, value):
        """
        Converts text values from self.conf_value based on type_matrix below
        check_matrix defines what kind of checks we can perform on a field
        value based on field name
        """
        f_type = self.type_matrix.get(field)
        if f_type:
            cast_to = f_type["type"]
        else:
            raise UnknownTypeException("Unknown type")

        if value is None:
            return None

        try:
            if cast_to == "int":
                return int(value)
            elif cast_to == "float":
                return float(value)
            elif cast_to == "unicode":
                return str(value)
        except ValueError as exc:
            raise InvalidValueException(exc)


class Rule(RuleBase):
    def __init__(
        self,
        config,
        type_matrix,
        struct_getter=RuleBase.default_dict_struct_getter,
        config_manipulator=None,
    ):
        """

        :param config: dict - contains rule configuration
                              example::
                                    {
                                        "field": "__OR__",
                                        "rules": [
                                            {
                                                "field": "__AND__",
                                                "rules": [
                                                    {
                                                        "op": "ge",
                                                        "field": "occurences",
                                                        "value": "10"
                                                    },
                                                    {
                                                        "op": "ge",
                                                        "field": "priority",
                                                        "value": "4"
                                                    }
                                                ]
                                            },
                                            {
                                                "op": "eq",
                                                "field": "http_status",
                                                "value": "500"
                                            }
                                        ]
                                    }
        :param type_matrix: dict - contains map of type casts
                                   example::
                                        {
                                        'http_status': 'int',
                                        'priority': 'unicode',
                                        }
        :param struct_getter: callable - used to grab the value of field from
                                         the structure passed to match() based
                                         on key, default

        """
        self.type_matrix = type_matrix
        self.config = config
        self.struct_getter = struct_getter
        self.config_manipulator = config_manipulator
        if config_manipulator:
            config_manipulator(self)

    def subrule_check(self, rule_config, struct):
        rule = Rule(
            rule_config, self.type_matrix, config_manipulator=self.config_manipulator
        )
        return rule.match(struct)

    def match(self, struct):
        """
        Check if rule matched for this specific report
        First tries report value, then tests tags in not found, then finally
        report group
        """
        field_name = self.config.get("field")
        test_value = self.config.get("value")

        if not field_name:
            return False

        if field_name == "__AND__":
            rule = AND(
                self.config["rules"],
                self.type_matrix,
                config_manipulator=self.config_manipulator,
            )
            return rule.match(struct)
        elif field_name == "__OR__":
            rule = OR(
                self.config["rules"],
                self.type_matrix,
                config_manipulator=self.config_manipulator,
            )
            return rule.match(struct)
        elif field_name == "__NOT__":
            rule = NOT(
                self.config["rules"],
                self.type_matrix,
                config_manipulator=self.config_manipulator,
            )
            return rule.match(struct)

        if test_value is None:
            return False

        try:
            struct_value = self.normalized_type(
                field_name, self.struct_getter(struct, field_name)
            )
        except (UnknownTypeException, InvalidValueException) as exc:
            log.error(str(exc))
            return False

        try:
            test_value = self.normalized_type(field_name, test_value)
        except (UnknownTypeException, InvalidValueException) as exc:
            log.error(str(exc))
            return False

        if self.config["op"] not in ("startswith", "endswith", "contains"):
            try:
                return getattr(operator, self.config["op"])(struct_value, test_value)
            except TypeError:
                return False
        elif self.config["op"] == "startswith":
            return struct_value.startswith(test_value)
        elif self.config["op"] == "endswith":
            return struct_value.endswith(test_value)
        elif self.config["op"] == "contains":
            return test_value in struct_value
        raise BadConfigException(
            "Invalid configuration, " "unknown operator: {}".format(self.config)
        )

    def __repr__(self):
        return "<Rule {} {}>".format(self.config.get("field"), self.config.get("value"))


class AND(Rule):
    def __init__(self, rules, *args, **kwargs):
        super(AND, self).__init__({}, *args, **kwargs)
        self.rules = rules

    def match(self, struct):
        return all([self.subrule_check(r_conf, struct) for r_conf in self.rules])


class NOT(Rule):
    def __init__(self, rules, *args, **kwargs):
        super(NOT, self).__init__({}, *args, **kwargs)
        self.rules = rules

    def match(self, struct):
        return all([not self.subrule_check(r_conf, struct) for r_conf in self.rules])


class OR(Rule):
    def __init__(self, rules, *args, **kwargs):
        super(OR, self).__init__({}, *args, **kwargs)
        self.rules = rules

    def match(self, struct):
        return any([self.subrule_check(r_conf, struct) for r_conf in self.rules])


class RuleService(object):
    @staticmethod
    def rule_from_config(config, field_mappings, labels_dict, manipulator_func=None):
        """
        Returns modified rule with manipulator function
        By default manipulator function replaces field id from labels_dict
        with current field id proper for the rule from fields_mappings

        because label X_X id might be pointing different value on next request
        when new term is returned from elasticsearch - this ensures things
        are kept 1:1 all the time
        """
        rev_map = {}
        for k, v in labels_dict.items():
            rev_map[(v["agg"], v["key"])] = k

        if manipulator_func is None:

            def label_rewriter_func(rule):
                field = rule.config.get("field")
                if not field or rule.config["field"] in [
                    "__OR__",
                    "__AND__",
                    "__NOT__",
                ]:
                    return

                to_map = field_mappings.get(rule.config["field"])

                # we need to replace series field with _AE_NOT_FOUND_ to not match
                # accidently some other field which happens to have the series that
                # was used when the alert was created
                if to_map:
                    to_replace = rev_map.get(
                        (to_map["agg"], to_map["key"]), "_AE_NOT_FOUND_"
                    )
                else:
                    to_replace = "_AE_NOT_FOUND_"

                rule.config["field"] = to_replace
                rule.type_matrix[to_replace] = {"type": "float"}

            manipulator_func = label_rewriter_func

        return Rule(config, {}, config_manipulator=manipulator_func)
