"""This package contains various classes to build check rulesets. The ruleset
also supports parallel rule evaluation, e.g. the two rules "A and B and C" and
"A and B and D" will only peform the checks for A and B once, then performs
check C and D and trigger a match action.

This program is free software: you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software
Foundation, either version 3 of the License, or (at your option) any later
version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
this program. If not, see <http://www.gnu.org/licenses/>.
"""

import re
import sys
import abc
import logging
from datetime import datetime, timezone

from aminer.util.History import LogarithmicBackoffHistory
from aminer.util.History import ObjectHistory

from aminer.analysis.AtomFilters import SubhandlerFilter
from aminer.AminerConfig import DEBUG_LOG_NAME, STAT_LOG_NAME
from aminer import AminerConfig
from aminer.events.EventInterfaces import EventHandlerInterface

result_string = "%s(%s)"


class MatchAction(metaclass=abc.ABCMeta):
    """This is the interface of all match actions."""

    @abc.abstractmethod
    def match_action(self, log_atom):
        """Invoke this method if a rule has matched.

        @param log_atom the LogAtom matching the rules.
        """


class EventGenerationMatchAction(MatchAction):
    """This generic match action forwards information about a rule match on
    parsed data to a list of event handlers."""

    def __init__(self, event_type, event_message, event_handlers):
        self.event_type = event_type
        self.event_message = event_message
        self.event_handlers = event_handlers
        if not isinstance(event_type, str):
            msg = "event_type has to be of type string."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise TypeError(msg)
        if len(event_type) == 0:
            msg = "event_type must not be empty."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise ValueError(msg)
        if not isinstance(event_message, str):
            msg = "event_message has to be of type string."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise TypeError(msg)
        if not isinstance(event_handlers, list) or not all(isinstance(x, EventHandlerInterface) for x in event_handlers):
            msg = "event_handlers has to be a list of EventHandlers."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise TypeError(msg)
        if len(event_handlers) == 0:
            msg = "event_handlers must not be empty."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise ValueError(msg)

    def match_action(self, log_atom):
        """Invoke this method if a rule has matched.

        @param log_atom the LogAtom matching the rules.
        """
        event_data = {}
        for handler in self.event_handlers:
            handler.receive_event(self.event_type, self.event_message, [log_atom.parser_match.match_element.annotate_match("")], event_data,
                                  log_atom, self)


class AtomFilterMatchAction(MatchAction, SubhandlerFilter):
    """This generic match rule forwards all rule matches to a list of
    AtomHandlerInterface instances using the SubhandlerFilter."""

    def __init__(self, subhandler_list, stop_when_handled_flag=False):
        SubhandlerFilter.__init__(self, subhandler_list, stop_when_handled_flag)

    def match_action(self, log_atom):
        """Invoke this method if a rule has matched.

        @param log_atom the LogAtom matching the rules.
        """
        return self.receive_atom(log_atom)


class MatchRule(metaclass=abc.ABCMeta):
    """This is the interface of all match rules."""

    log_success = 0
    log_total = 0

    @abc.abstractmethod
    def match(self, log_atom):
        """Check if this rule matches.

        On match an optional match_action could be triggered.
        """

    def log_statistics(self, rule_id):
        """Log statistics of an MatchRule.

        Override this method for more sophisticated statistics output of
        the MatchRule.
        """
        if AminerConfig.STAT_LEVEL > 0:
            logging.getLogger(STAT_LOG_NAME).info("Rule '%s' processed %d out of %d log atoms successfully in the last 60 minutes.",
                                                  rule_id, self.log_success, self.log_total)
        self.log_success = 0
        self.log_total = 0
        if hasattr(self, "sub_rules"):
            for i, rule in enumerate(self.sub_rules):
                rule.log_statistics(rule_id + "." + rule.__class__.__name__ + str(i))
        if hasattr(self, "rule_lookup_dict"):
            for i, rule_key in enumerate(self.rule_lookup_dict):
                rule = self.rule_lookup_dict[rule_key]
                rule.log_statistics(rule_id + "." + rule.__class__.__name__ + str(i))
        if hasattr(self, "default_rule"):
            self.default_rule.log_statistics(rule_id + ".default_rule." + self.default_rule.__class__.__name__)
        if hasattr(self, "sub_rule"):
            self.sub_rule.log_statistics(rule_id + "." + self.sub_rule.__class__.__name__)


class AndMatchRule(MatchRule):
    """This class provides a rule to match all subRules (logical and)."""

    def __init__(self, sub_rules, match_action=None):
        """Create the rule.

        @param match_action if None, no action is performed.
        """
        self.sub_rules = sub_rules
        self.match_action = match_action
        if not isinstance(sub_rules, list) or not all(isinstance(x, MatchRule) for x in sub_rules):
            msg = "sub_rules has to be a list of MatchRules."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise TypeError(msg)
        if len(sub_rules) < 2:
            msg = "At least two sub rules must exist in the AndMatchRule."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise ValueError(msg)
        if match_action is not None and not isinstance(match_action, MatchAction):
            msg = "match_action has to be of type MatchAction."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise TypeError(msg)

    def match(self, log_atom):
        """Check if this rule matches. Rule evaluation will stop when the first
        match fails.

        If a matchAction is attached to this rule, it will be invoked at the end of all checks.
        @return True when all subrules matched.
        """
        self.log_total += 1
        for rule in self.sub_rules:
            if not rule.match(log_atom):
                return False
        if self.match_action is not None:
            self.match_action.match_action(log_atom)
        self.log_success += 1
        return True

    def __str__(self):
        result = ""
        preamble = ""
        for match_element in self.sub_rules:
            result += result_string % (preamble, match_element)
            preamble = " and "
        return result


class OrMatchRule(MatchRule):
    """This class provides a rule to match any subRules (logical or)."""

    def __init__(self, sub_rules, match_action=None):
        """Create the rule.

        @param match_action if None, no action is performed.
        """
        self.sub_rules = sub_rules
        self.match_action = match_action
        if not isinstance(sub_rules, list) or not all(isinstance(x, MatchRule) for x in sub_rules):
            msg = "sub_rules has to be a list of MatchRules."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise TypeError(msg)
        if len(sub_rules) < 2:
            msg = "At least two sub rules must exist in the AndMatchRule."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise ValueError(msg)
        if match_action is not None and not isinstance(match_action, MatchAction):
            msg = "match_action has to be of type MatchAction."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise TypeError(msg)

    def match(self, log_atom):
        """Check if this rule matches. Rule evaluation will stop when the first
        match succeeds.

        If a matchAction is attached to this rule, it will be invoked after the first match.
        @return True when any subrule matched.
        """
        self.log_total += 1
        for rule in self.sub_rules:
            if rule.match(log_atom):
                if self.match_action is not None:
                    self.match_action.match_action(log_atom)
                self.log_success += 1
                return True
        return False

    def __str__(self):
        result = ""
        preamble = ""
        for match_element in self.sub_rules:
            result += result_string % (preamble, match_element)
            preamble = " or "
        return result


class ParallelMatchRule(MatchRule):
    """This class is a rule testing all the subrules in parallel.

    From the behaviour it is similar to the OrMatchRule, returning true
    if any subrule matches. The difference is that matching will not
    stop after the first positive match. This does only make sense when
    all subrules have match actions associated.
    """

    def __init__(self, sub_rules, match_action=None):
        """Create the rule.

        @param match_action if None, no action is performed.
        """
        self.sub_rules = sub_rules
        self.match_action = match_action
        if not isinstance(sub_rules, list) or not all(isinstance(x, MatchRule) for x in sub_rules):
            msg = "sub_rules has to be a list of MatchRules."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise TypeError(msg)
        if len(sub_rules) < 2:
            msg = "At least two sub rules must exist in the AndMatchRule."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise ValueError(msg)
        if match_action is not None and not isinstance(match_action, MatchAction):
            msg = "match_action has to be of type MatchAction."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise TypeError(msg)

    def match(self, log_atom):
        """Check if any of the subrules rule matches. The matching procedure
        will not stop after the first positive match.

        If a matchAction is attached to this rule, it will be invoked at the end of all checks.
        @return True when any subrule matched.
        """
        self.log_total += 1
        match_flag = False
        for rule in self.sub_rules:
            if rule.match(log_atom):
                match_flag = True
        if match_flag and (self.match_action is not None):
            self.match_action.match_action(log_atom)
        if match_flag:
            self.log_success += 1
        return match_flag

    def __str__(self):
        result = ""
        preamble = ""
        for match_element in self.sub_rules:
            result += result_string % (preamble, match_element)
            preamble = " por "
        return result


class ValueDependentDelegatedMatchRule(MatchRule):
    """This class is a rule delegating rule checking to subrules depending on
    values found within the parser_match.

    The result of this rule is the result of the selected delegation
    rule.
    """

    def __init__(self, target_path_list, rule_lookup_dict, default_rule=None, match_action=None):
        """
        Create the rule.
        @param target_path_list with value paths that are used to extract the lookup keys for rule_lookup_dict. If value lookup fails, None
               will be used for lookup.
        @param rule_lookup_dict dictionary with tuple containing values for valuePathList as key and target rule as value.
        @param default_rule when not none, this rule will be executed as default. Otherwise, when rule lookup failed, False will
               be returned unconditionally.
        @param match_action if None, no action is performed.
        """
        self.target_path_list = target_path_list
        self.rule_lookup_dict = rule_lookup_dict
        self.default_rule = default_rule
        self.match_action = match_action
        if not isinstance(target_path_list, list) or not all(isinstance(x, str) for x in target_path_list):
            msg = "target_path_list has to be a list of String."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise TypeError(msg)
        if len(target_path_list) == 0 or not all(x != "" for x in target_path_list):
            msg = "target_path_list must not be empty."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise ValueError(msg)
        if not isinstance(rule_lookup_dict, dict) or not all(isinstance(x, tuple) and len(x) != 0 for x in rule_lookup_dict.keys()) or \
                not all(isinstance(x, MatchRule) for x in rule_lookup_dict.values()):
            msg = "rule_lookup_dict has to be a dict of with tuples as keys and MatchRules as values."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise TypeError(msg)
        if default_rule is not None and not isinstance(default_rule, MatchRule):
            msg = "default_rule has to be of type MatchRule."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise TypeError(msg)
        if match_action is not None and not isinstance(match_action, MatchAction):
            msg = "match_action has to be of type MatchAction."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise TypeError(msg)

    def match(self, log_atom):
        """Try to locate a rule for delegation or use the default rule.

        @return True when selected delegation rule matched.
        """
        self.log_total += 1
        match_dict = log_atom.parser_match.get_match_dictionary()
        value_list = []
        for path in self.target_path_list:
            value_element = match_dict.get(path)
            if value_element is not None:
                value_list.append(value_element.match_object)
        if len(value_list) > 0:
            value = tuple(value_list)
        else:
            value = None
        rule = self.rule_lookup_dict.get(value, self.default_rule)
        if rule is None:
            return False
        if rule.match(log_atom):
            if self.match_action is not None:
                self.match_action.match_action(log_atom)
            self.log_success += 1
            return True
        return False

    def __str__(self):
        result = "ValueDependentDelegatedMatchRule"
        return result


class NegationMatchRule(MatchRule):
    """Match elements of this class return true when the subrule did not
    match."""

    def __init__(self, sub_rule, match_action=None):
        self.sub_rule = sub_rule
        self.match_action = match_action
        if not isinstance(sub_rule, MatchRule):
            msg = "sub_rule has to be of type MatchRule."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise TypeError(msg)
        if match_action is not None and not isinstance(match_action, MatchAction):
            msg = "match_action has to be of type MatchAction."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise TypeError(msg)

    def match(self, log_atom):
        """Check if this rule matches.

        On match an optional match_action could be triggered.
        """
        self.log_total += 1
        if self.sub_rule.match(log_atom):
            return False
        if self.match_action is not None:
            self.match_action.match_action(log_atom)
        self.log_success += 1
        return True

    def __str__(self):
        return f"not {self.sub_rule}"


class PathExistsMatchRule(MatchRule):
    """Match elements of this class return true when the given target_path was
    found in the parsed match data."""

    def __init__(self, target_path, match_action=None):
        self.target_path = target_path
        self.match_action = match_action
        if not isinstance(target_path, str):
            msg = "target_path has to be of type String."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise TypeError(msg)
        if target_path == "":
            msg = "target_path must not be empty."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise ValueError(msg)
        if match_action is not None and not isinstance(match_action, MatchAction):
            msg = "match_action has to be of type MatchAction."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise TypeError(msg)

    def match(self, log_atom):
        """Check if this rule matches.

        On match an optional match_action could be triggered.
        """
        self.log_total += 1
        if self.target_path in log_atom.parser_match.get_match_dictionary():
            if self.match_action is not None:
                self.match_action.match_action(log_atom)
            self.log_success += 1
            return True
        return False

    def __str__(self):
        return f"hasPath({self.target_path})"


class ValueMatchRule(MatchRule):
    """Match elements of this class return true when the given target_path
    exists and has exactly the given parsed value."""

    def __init__(self, target_path, value, match_action=None):
        self.target_path = target_path
        self.value = value
        self.match_action = match_action
        if not isinstance(target_path, str):
            msg = "target_path has to be of type String."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise TypeError(msg)
        if target_path == "":
            msg = "target_path must not be empty."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise ValueError(msg)
        if isinstance(value, (str, bytes)) and len(value) == 0:
            msg = "value must not be empty."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise ValueError(msg)
        if match_action is not None and not isinstance(match_action, MatchAction):
            msg = "match_action has to be of type MatchAction."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise TypeError(msg)

    def match(self, log_atom):
        """Check if this rule matches.

        On match an optional match_action could be triggered.
        """
        self.log_total += 1
        test_value = log_atom.parser_match.get_match_dictionary().get(self.target_path, None)
        if test_value is not None:
            if isinstance(self.value, bytes) and isinstance(test_value.match_object, str) and test_value.match_object is not None:
                test_value.match_object = test_value.match_object.encode()
            elif isinstance(self.value, str) and isinstance(test_value.match_object, bytes) and self.value is not None:
                self.value = self.value.encode()
            elif not isinstance(self.value, type(test_value.match_object)):
                raise TypeError(f"The type of the value of the ValueMatchRule does not match the test_value. value: {type(self.value)}, "
                                f"test_value: {type(test_value.match_object)}")
        if (test_value is not None) and (test_value.match_object == self.value):
            if self.match_action is not None:
                self.match_action.match_action(log_atom)
            self.log_success += 1
            return True
        return False

    def __str__(self):
        if isinstance(self.value, bytes):
            self.value = self.value.decode()
        return f"value({self.target_path})=={self.value}"


class ValueListMatchRule(MatchRule):
    """Match elements of this class return true when the given path exists and
    has exactly one of the values included in the value list."""

    def __init__(self, target_path, target_value_list, match_action=None):
        self.target_path = target_path
        self.target_value_list = target_value_list
        self.match_action = match_action
        if not isinstance(target_path, str):
            msg = "target_path has to be of type String."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise TypeError(msg)
        if target_path == "":
            msg = "target_path must not be empty."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise ValueError(msg)
        if not isinstance(target_value_list, list):
            msg = "target_value_list must be of type list."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise TypeError(msg)
        if not all(isinstance(x, (bytes, str)) and len(x) != 0 or not isinstance(x, (bytes, str)) for x in target_value_list) or \
                len(target_value_list) == 0:
            msg = "target values must not be empty."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise ValueError(msg)
        if match_action is not None and not isinstance(match_action, MatchAction):
            msg = "match_action has to be of type MatchAction."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise TypeError(msg)

    def match(self, log_atom):
        """Check if this rule matches.

        On match an optional match_action could be triggered.
        """
        self.log_total += 1
        test_value = log_atom.parser_match.get_match_dictionary().get(self.target_path)
        if (test_value is not None) and (test_value.match_object in self.target_value_list):
            if self.match_action is not None:
                self.match_action.match_action(log_atom)
            self.log_success += 1
            return True
        return False

    def __str__(self):
        return f"value({' '.join([str(value) for value in self.target_value_list])}) in {self.target_path}"


class ValueRangeMatchRule(MatchRule):
    """Match elements of this class return true when the given target_path
    exists and the value is included in [lower, upper] range."""

    def __init__(self, target_path, lower_limit, upper_limit, match_action=None):
        self.target_path = target_path
        self.lower_limit = lower_limit
        self.upper_limit = upper_limit
        self.match_action = match_action
        if not isinstance(target_path, str):
            msg = "target_path has to be of type String."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise TypeError(msg)
        if isinstance(lower_limit, bool) or not isinstance(lower_limit, (int, float)):
            msg = "lower_limit has to be of type int or float."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise TypeError(msg)
        if isinstance(upper_limit, bool) or not isinstance(upper_limit, (int, float)):
            msg = "upper_limit has to be of type int or float."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise TypeError(msg)
        if lower_limit >= upper_limit:
            msg = "lower_limit must be smaller than upper_limit."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise ValueError(msg)
        if target_path == "":
            msg = "target_path must not be empty."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise ValueError(msg)
        if match_action is not None and not isinstance(match_action, MatchAction):
            msg = "match_action has to be of type MatchAction."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise TypeError(msg)

    def match(self, log_atom):
        """Check if this rule matches.

        On match an optional match_action could be triggered.
        """
        self.log_total += 1
        test_value = log_atom.parser_match.get_match_dictionary().get(self.target_path, None)
        if test_value is None:
            return False
        test_value = test_value.match_object
        if self.lower_limit <= test_value <= self.upper_limit:
            if self.match_action is not None:
                self.match_action.match_action(log_atom)
            self.log_success += 1
            return True
        return False

    def __str__(self):
        return f"value({self.target_path}) inrange ({self.lower_limit}, {self.upper_limit})"


class StringRegexMatchRule(MatchRule):
    """Elements of this class return true when the given path exists and the
    string repr of the value matches the regular expression."""

    def __init__(self, target_path, match_regex, match_action=None):
        self.target_path = target_path
        self.match_regex = match_regex
        self.match_action = match_action
        if not isinstance(target_path, str):
            msg = "target_path has to be of type String."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise TypeError(msg)
        if target_path == "":
            msg = "target_path must not be empty."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise ValueError(msg)
        if not isinstance(match_regex, re.Pattern):
            msg = "match_regex has to be of type re.Pattern."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise TypeError(msg)
        if match_action is not None and not isinstance(match_action, MatchAction):
            msg = "match_action has to be of type MatchAction."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise TypeError(msg)

    def match(self, log_atom):
        """Check if this rule matches.

        On match an optional match_action could be triggered.
        """
        self.log_total += 1
        # Use the class object as marker for nonexisting entries
        test_value = log_atom.parser_match.get_match_dictionary().get(self.target_path, None)
        if (test_value is None) or (self.match_regex.match(test_value.match_string) is None):
            return False
        if self.match_action is not None:
            self.match_action.match_action(log_atom)
        self.log_success += 1
        return True

    def __str__(self):
        return f"string({self.target_path}) =regex= {self.match_regex.pattern}"


class ModuloTimeMatchRule(MatchRule):
    """Match elements of this class return true when the following conditions
    are met.

    The given target_path exists, denotes a datetime object and the
    seconds since 1970 from that date modulo the given value are
    included in [lower, upper] range.
    """

    def __init__(self, target_path, seconds_modulo, lower_limit, upper_limit, match_action=None, tzinfo=None):
        """
        @param target_path the target_path to the datetime object to use to evaluate the modulo time rules on.
        When None, the default timestamp associated with the match is used.
        """
        self.target_path = target_path
        self.seconds_modulo = seconds_modulo
        self.lower_limit = lower_limit
        self.upper_limit = upper_limit
        self.match_action = match_action
        self.tzinfo = tzinfo
        if tzinfo is None:
            self.tzinfo = datetime.now(timezone.utc).astimezone().tzinfo
        if not isinstance(target_path, str):
            msg = "target_path has to be of type String."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise TypeError(msg)
        if target_path == "":
            msg = "target_path must not be empty."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise ValueError(msg)
        if isinstance(seconds_modulo, bool) or not isinstance(seconds_modulo, int):
            msg = "seconds_modulo has to be of type int."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise TypeError(msg)
        if seconds_modulo <= 0:
            msg = "seconds_modulo must be bigger than zero."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise ValueError(msg)
        if isinstance(lower_limit, bool) or not isinstance(lower_limit, int):
            msg = "lower_limit has to be of type int."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise TypeError(msg)
        if isinstance(upper_limit, bool) or not isinstance(upper_limit, int):
            msg = "upper_limit has to be of type int."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise TypeError(msg)
        if lower_limit >= upper_limit:
            msg = "lower_limit must be smaller than upper_limit."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise ValueError(msg)
        if lower_limit < 0:
            msg = "lower_limit must be greater than or equal zero."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise ValueError(msg)
        if upper_limit <= 0:
            msg = "upper_limit must be greater than zero."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise ValueError(msg)
        if upper_limit > seconds_modulo:
            msg = "upper_limit can not be greater than seconds_modulo."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise ValueError(msg)
        if match_action is not None and not isinstance(match_action, MatchAction):
            msg = "match_action has to be of type MatchAction."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise TypeError(msg)
        if not isinstance(self.tzinfo, timezone):
            msg = "tzinfo has to be of type datetime.timezone."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise TypeError(msg)

    def match(self, log_atom):
        """Check if this rule matches.

        On match an optional match_action could be triggered.
        """
        self.log_total += 1
        test_value = None
        if self.target_path is None:
            test_value = log_atom.get_timestamp()
        else:
            time_match = log_atom.parser_match.get_match_dictionary().get(self.target_path, None)
            if time_match is None:
                return False
            test_value = time_match.match_object + datetime.now(self.tzinfo).utcoffset().total_seconds()

        if test_value is None:
            return False
        test_value %= self.seconds_modulo
        if self.lower_limit <= test_value <= self.upper_limit:
            if self.match_action is not None:
                self.match_action.match_action(log_atom)
            self.log_success += 1
            return True
        return False


class ValueDependentModuloTimeMatchRule(MatchRule):
    """Match elements of this class return true when the following conditions
    are met.

    The given path exists, denotes a datetime object and the seconds
    since 1970 rom that date modulo the given value are included in a
    [lower, upper] range selected by values from the match.
    """

    def __init__(self, target_path, seconds_modulo, target_path_list=None, limit_lookup_dict=None, default_limit=None, match_action=None,
                 tzinfo=None):
        """
        @param target_path the target_path to the datetime object to use to evaluate the modulo time rules on.
        When None, the default timestamp associated with the match is used.
        @param default_limit use this default limit when limit lookup failed. Without a default limit, a failed lookup will cause
        the rule not to match.
        """
        self.target_path = target_path
        self.seconds_modulo = seconds_modulo
        self.target_path_list = target_path_list
        self.limit_lookup_dict = limit_lookup_dict
        self.default_limit = default_limit
        self.match_action = match_action
        self.tzinfo = tzinfo
        if tzinfo is None:
            self.tzinfo = datetime.now(timezone.utc).astimezone().tzinfo
        if not isinstance(target_path, str):
            msg = "target_path has to be of type String."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise TypeError(msg)
        if target_path == "":
            msg = "target_path must not be empty."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise ValueError(msg)
        if isinstance(seconds_modulo, bool) or not isinstance(seconds_modulo, int):
            msg = "seconds_modulo has to be of type int."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise TypeError(msg)
        if seconds_modulo <= 0:
            msg = "seconds_modulo must be bigger than zero."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise ValueError(msg)
        if target_path_list is not None and (not isinstance(target_path_list, list) or
                                             not all(isinstance(x, str) and len(x) > 0 for x in target_path_list)):
            msg = "target_path_list has to be a list of strings."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise TypeError(msg)
        if limit_lookup_dict is not None and (
                not isinstance(limit_lookup_dict, dict) or None in limit_lookup_dict.keys() or
                not all(isinstance(x, list) and all(not isinstance(y, bool) and isinstance(y, (int, float)) for y in x) and len(x) == 2 and
                        x[0] < x[1] <= seconds_modulo for x in limit_lookup_dict.values())):
            msg = "limit_lookup_dict has to be of type dict with a list of two integer or float limit values as values. " \
                  "The first limit value must be smaller than the second and both must be smaller than seconds_modulo."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise TypeError(msg)
        if default_limit is not None and (
                not isinstance(default_limit, list) or
                not all(not isinstance(x, bool) and isinstance(x, (int, float)) for x in default_limit) or
                len(default_limit) != 2 or default_limit[0] > default_limit[1] or default_limit[1] > seconds_modulo):
            msg = "default_limit has to be a list with two integer or float limit values. The first value must be smaller than the " \
                  "second and both must be smaller than seconds_modulo."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise TypeError(msg)
        if default_limit is None and (not limit_lookup_dict or not target_path_list):
            msg = "Either default_limit or limit_lookup_dict and target_path_list must not be None."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise ValueError(msg)
        if match_action is not None and not isinstance(match_action, MatchAction):
            msg = "match_action has to be of type MatchAction."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise TypeError(msg)
        if not isinstance(self.tzinfo, timezone):
            msg = "tzinfo has to be of type datetime.timezone."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise TypeError(msg)

    def match(self, log_atom):
        """Check if this rule matches.

        On match an optional match_action could be triggered.
        """
        self.log_total += 1
        match_dict = log_atom.parser_match.get_match_dictionary()
        value_list = []
        for path in self.target_path_list:
            value_element = match_dict.get(path)
            if value_element is not None:
                value_list.append(value_element.match_object)

        if len(value_list) > 0:
            value = value_list[0]
        else:
            value = None
        limits = self.limit_lookup_dict.get(value, self.default_limit)
        if limits is None:
            return False

        if self.target_path is None:
            test_value = log_atom.get_timestamp()
        else:
            time_match = log_atom.parser_match.get_match_dictionary().get(self.target_path, None)
            if time_match is None:
                return False
            test_value = time_match.match_object + datetime.now(self.tzinfo).utcoffset().total_seconds()

        test_value %= self.seconds_modulo
        if limits[0] <= test_value <= limits[1]:
            if self.match_action is not None:
                self.match_action.match_action(log_atom)
            self.log_success += 1
            return True
        return False


class IPv4InRFC1918MatchRule(MatchRule):
    """Match elements of this class return true when the path matches and
    contains a valid IPv4 address from the RFC1918 private IP ranges.

    This could also be done by distinct range match elements, but as
    this kind of matching is common, have an own element for it.
    """

    def __init__(self, target_path, match_action=None):
        self.target_path = target_path
        self.match_action = match_action
        if not isinstance(target_path, str):
            msg = "target_path has to be of type String."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise TypeError(msg)
        if target_path == "":
            msg = "target_path must not be empty."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise ValueError(msg)
        if match_action is not None and not isinstance(match_action, MatchAction):
            msg = "match_action has to be of type MatchAction."
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise TypeError(msg)

    def match(self, log_atom):
        """Check if this rule matches.

        On match an optional match_action could be triggered.
        """
        self.log_total += 1
        match_element = log_atom.parser_match.get_match_dictionary().get(self.target_path)
        if (match_element is None) or not isinstance(match_element.match_object, int):
            return False
        value = match_element.match_object
        if ((value & 0xff000000) == 0xa000000) or ((value & 0xfff00000) == 0xac100000) or ((value & 0xffff0000) == 0xc0a80000):
            if self.match_action is not None:
                self.match_action.match_action(log_atom)
            self.log_success += 1
            return True
        return False

    def __str__(self):
        return f"hasPath({self.target_path})"


class DebugMatchRule(MatchRule):
    """This rule can be inserted into a normal ruleset just to see when a match
    attempt is made.

    It just prints out the current log_atom that is evaluated. The match
    action is always invoked when defined, no matter which match result
    is returned.
    """

    def __init__(self, debug_match_result=False, match_action=None):
        self.debug_match_result = debug_match_result
        self.match_action = match_action

    def match(self, log_atom):
        """Check if this rule matches.

        On match an optional match_action could be triggered.
        """
        self.log_total += 1
        print(f'Rules.DebugMatchRule: triggered while handling "{repr(log_atom.parser_match.match_element.match_string)}"',
              file=sys.stderr)
        if self.match_action is not None:
            self.match_action.match_action(log_atom)
        self.log_success += 1
        return self.debug_match_result

    def __str__(self):
        return f"{self.debug_match_result}"


class DebugHistoryMatchRule(MatchRule):
    """This rule can be inserted into a normal ruleset just to see when a match
    attempt is made.

    It just adds the evaluated log_atom to a ObjectHistory.
    """

    def __init__(self, object_history=None, debug_match_result=False, match_action=None):
        """Create a DebugHistoryMatchRule object.

        @param object_history use this ObjectHistory to collect the LogAtoms. When None, a default LogarithmicBackoffHistory for 10 items.
        """
        if object_history is None:
            object_history = LogarithmicBackoffHistory(10)
        elif not isinstance(object_history, ObjectHistory):
            msg = "object_history is not an instance of ObjectHistory"
            logging.getLogger(DEBUG_LOG_NAME).error(msg)
            raise Exception(msg)
        self.object_history = object_history
        self.debug_match_result = debug_match_result
        self.match_action = match_action

    def match(self, log_atom):
        """Check if this rule matches.

        On match an optional match_action could be triggered.
        """
        self.log_total += 1
        self.object_history.add_object(log_atom)
        if self.match_action is not None:
            self.match_action.match_action(log_atom)
        self.log_success += 1
        return self.debug_match_result

    def get_history(self):
        """Get the history object from this debug rule."""
        return self.object_history
