# -*- coding: utf-8 -*-
#
# Copyright (c) 2020 OpenStack Foundation.
# All Rights Reserved.
#
#    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 abc

from oslo_utils import strutils
from oslo_utils import timeutils
from oslo_utils import uuidutils

from tacker.common import exceptions as exception


registered_filters = {}
SUPPORTED_OP_ONE = ['eq', 'neq', 'gt', 'lt', 'gte', 'lte']
SUPPORTED_OP_MULTI = ['in', 'nin', 'cont', 'ncont']


class BaseFilter(object, metaclass=abc.ABCMeta):
    """Abstract base class for Filter classes."""

    @abc.abstractmethod
    def __str__(self):
        """String representation of the filter tree rooted at this node."""

        pass

    @abc.abstractmethod
    def __call__(self, target):
        """Triggers if instance of the class is called.

        Performs the checks against operators, attribute and datatype
        of the value. Raises exception if it's invalid and finally attribute
        is mapped to the database model that's present in the target dict.
        """

        pass


class Filter(BaseFilter):
    SUPPORTED_OPERATORS = None

    FILTER_OPERATOR_SPEC_MAPPING = {
        'eq': '==',
        'neq': '!=',
        'in': 'in',
        'nin': 'not_in',
        'gt': '>',
        'gte': '>=',
        'lt': '<',
        'lte': '<=',
        'cont': 'in',
        'ncont': 'not_in'
    }

    OPERATOR_SUPPORTED_DATA_TYPES = {
        'eq': ['uuid', 'string', 'number', 'enum', 'boolean',
               'key_value_pair'],
        'neq': ['uuid', 'string', 'number', 'enum', 'boolean',
                'key_value_pair'],
        'in': ['uuid', 'string', 'number', 'enum', 'key_value_pair'],
        'nin': ['uuid', 'string', 'number', 'enum', 'key_value_pair'],
        'gt': ['uuid', 'string', 'number', 'datetime', 'key_value_pair'],
        'gte': ['uuid', 'string', 'number', 'datetime', 'key_value_pair'],
        'lt': ['uuid', 'string', 'number', 'datetime', 'key_value_pair'],
        'lte': ['uuid', 'string', 'number', 'datetime', 'key_value_pair'],
        'cont': ['uuid', 'string', 'key_value_pair'],
        'ncont': ['uuid', 'string', 'key_value_pair'],
    }

    def __init__(self, operator, attribute, values):
        self.operator = operator
        self.attribute = attribute
        self.values = values

    def __str__(self):
        """Return a string representation of this filter."""

        return '%s,%s,%s' % (self.operator, self.attribute,
            ",".join(self.values))

    def _attribute_special_field(self, target):
        """Check if an attribute is a special field in the target

        Look for attributes in the target that ends with '*' as
        these are special attributes whose type could be 'key_value'
        which requires special treatment. For example
        if attribute in target is 'userDefinedData/*' and if self.attribute
        is userDefinedData/key1, then it's valid even though there is no
        exact match in the target because key/value pair values are
        dynamic.
        """
        special_attributes = [attribute for attribute in target.keys() if '*'
            in attribute]

        for attribute in special_attributes:
            field = attribute.split('*')[0]
            if self.attribute.startswith(field):
                return attribute

    def _validate_operators(self):
        if not self.operator:
            msg = ("Rule '%(rule)s' cannot contain operator")
            raise exception.ValidationError(msg % {"rule": self})

        if self.SUPPORTED_OPERATORS and self.operator not in \
                self.SUPPORTED_OPERATORS:
            msg = ("Rule '%(rule)s' contains invalid operator "
                   "'%(operator)s'")
            raise exception.ValidationError(msg % {"rule": self,
                "operator": self.operator})

    def _validate_attribute_name(self, target):
        if not self.attribute:
            msg = ("Rule '%(rule)s' doesn't contain attribute name")
            raise exception.ValidationError(msg % {"rule": self})

        if '*' in self.attribute:
            msg = ("Rule '%(rule)s' contains invalid attribute name "
                   "'%(attribute)s'")
            raise exception.ValidationError(msg % {"rule": self,
                "attribute": self.attribute})

        if target and self.attribute not in target:
            if not self._attribute_special_field(target):
                msg = ("Rule '%(rule)s' contains invalid attribute name "
                       "'%(attribute)s'")
                raise exception.ValidationError(msg % {"rule": self,
                    "attribute": self.attribute})

    def _handle_string(self, value):
        if value[0] == "'" and value[-1] == "'":
            value = value.strip("'")
            # The logic below enforces single quotes to be in pairs.
            # Raises exception otherwise. It also replaces a pair of
            # single quotes with one single quote.
            # NFV_SOL013 Section 5.2.2
            num_quotes = value.count("'")
            value = value.replace("''", "'")
            if (value.count("'") * 2) != num_quotes:
                msg = ("Rule '%(rule)s' value doesn't have single "
                       "quotes in pairs")
                raise exception.ValidationError(msg % {"rule": self})
        elif any(c in value for c in [",", ")", "'"]):
            msg = ("Rule '%(rule)s' value must be enclosed in "
                   "single quotes when it contains either of "
                   "comma, single quote, closing bracket")
            raise exception.ValidationError(msg % {"rule": self})
        return value

    def _handle_values(self, target):
        special_attribute = self._attribute_special_field(target)
        if special_attribute:
            attribute_info = target.get(special_attribute)
        else:
            attribute_info = target.get(self.attribute)

        if attribute_info[1] in ['string', 'key_value_pair']:
            values = [self._handle_string(v) for v in self.values]
            self.values = values

    def _validate_data_type(self, target):
        if not self.values:
            msg = ("Rule '%(rule)s' contains empty value")
            raise exception.ValidationError(msg % {"rule": self})

        special_attribute = self._attribute_special_field(target)
        if special_attribute:
            attribute_info = target.get(special_attribute)
        else:
            attribute_info = target.get(self.attribute)

        for value in self.values:
            error = False
            if attribute_info[1] == 'string' and not isinstance(value, str):
                error = True
            elif attribute_info[1] == 'number':
                if not strutils.is_int_like(value):
                    error = True
            elif attribute_info[1] == 'uuid':
                if not uuidutils.is_uuid_like(value):
                    error = True
            elif attribute_info[1] == 'datetime':
                try:
                    timeutils.parse_isotime(value)
                except ValueError:
                    error = True
            elif attribute_info[1] == 'enum':
                if value not in attribute_info[3]:
                    msg = ("Rule '%(rule)s' contains data type '%(type)s' "
                           "with invalid value. It should be one of "
                           "%(valid_value)s")
                    raise exception.ValidationError(msg % {"rule": self,
                        "valid_value": ",".join(attribute_info[3]),
                        'type': attribute_info[1]})

            if error:
                msg = ("Rule '%(rule)s' contains invalid data type for value "
                       "'%(value)s'. The data type should be '%(type)s'")
                raise exception.ValidationError(msg % {"rule": self,
                    "value": value,
                    'type': attribute_info[1]})

            # Also, check whether the data type is supported by operator
            if attribute_info[1] not in \
                    self.OPERATOR_SUPPORTED_DATA_TYPES.get(self.operator):
                msg = ("Rule '%(rule)s' contains operator '%(operator)s' "
                       "which doesn't support data type '%(type)s' for "
                       "attribute '%(attribute)s'")
                raise exception.ValidationError(msg % {"rule": self,
                                "operator": self.operator,
                                'type': attribute_info[1],
                                'attribute': self.attribute})

    def generate_expression(self, target, multiple_values=False):
        special_attribute = self._attribute_special_field(target)
        if special_attribute:
            attribute_info = target.get(special_attribute)
        else:
            attribute_info = target.get(self.attribute)

        attributes = attribute_info[0].split('.')
        key_token = self.attribute.split('/')[-1]
        if attribute_info[1] == 'key_value_pair':
            filter_spec = []
            expression_key = {'field': attribute_info[2]['key_column'],
                'model': attribute_info[2]['model'],
                'value': key_token,
                'op': self.FILTER_OPERATOR_SPEC_MAPPING.get(self.operator)}
            expression_value = {'field': attribute_info[2]['value_column'],
                'model': attribute_info[2]['model'],
                'value': self.values if multiple_values else self.values[0],
                'op': self.FILTER_OPERATOR_SPEC_MAPPING.get(self.operator)}
            filter_spec.append(expression_key)
            filter_spec.append(expression_value)
            expression = {'and': filter_spec}
        else:
            expression = {'field': attributes[-1],
                'model': attribute_info[2],
                'value': self.values if multiple_values else self.values[0],
                'op': self.FILTER_OPERATOR_SPEC_MAPPING.get(self.operator)}

        return expression


class AndFilter(BaseFilter):
    def __init__(self, filter_rule):
        self.filter_rules = filter_rule

    def __str__(self):
        """Return a string representation of this filter."""

        return '(%s)' % ' and '.join(str(r) for r in self.filter_rules)

    def __call__(self, target):
        """Run through this filter and maps it to the database model

        :returns
            A dict containing list of filter-specs required by
            sqlalchemy-filter.
        Example::

        filter=(eq,onboardingState,'onboarded');(eq,softwareImages/size, 10)

        Result would be:
        {
          'and': [
              {
                  'field': 'onboarding_state', 'model': 'Foo',
                  'value': "'onboarded'", 'op': '=='
              },
              {
                 'field': 'size', 'model': 'Foo',
                 'value': '10', 'op': '=='
              }
          ]
        }
        """
        filter_spec = []
        for filter_rule in self.filter_rules:
            result = filter_rule(target)
            filter_spec.append(result)

        return {'and': filter_spec}

    def add_filter_rule(self, filter_rule):
        """Adds filter rule to be tested.

        Allows addition of another filter rule to the list of filter rules
        that will be tested.

        :returns: self
        :rtype: :class:`.AndFilter`
        """

        self.filter_rules.append(filter_rule)
        return self


def register(name, func=None):
    # Perform the actual decoration by registering the function or
    # class.  Returns the function or class for compliance with the
    # decorator interface.
    def decorator(func):
        registered_filters[name] = func
        return func

    # If the function or class is given, do the registration
    if func:
        return decorator(func)

    return decorator


@register('simple_filter_expr_one')
class SimpleFilterExprOne(Filter):

    SUPPORTED_OPERATORS = SUPPORTED_OP_ONE

    def __call__(self, target):
        """Run through this filter and maps it to the database model

        :returns
            A dict containing list of filter-specs required by
            sqlalchemy-filter.
        Example::
        operator=eq, attribute=onBoardingState, and value='onboarded', then
        it would be mapped to following expression.
            {
                'field': 'onboarding_state', -> Mapped to the version field
                'model': 'Foo', -> Mapped to database model
                'value': "onboarded", -> Value to be used for filtering
                                         records
                'op': '==', -> Operator for comparison
            },
        """

        self._validate_operators()
        self._validate_attribute_name(target)
        self._validate_data_type(target)
        self._handle_values(target)

        return self.generate_expression(target, multiple_values=False)

    def _validate_operators(self):
        super(SimpleFilterExprOne, self)._validate_operators()
        if self.values and isinstance(self.values, list) and \
                len(self.values) > 1:
            msg = _("Rule '%(rule)s' contains operator '%(operator)s' "
                    "which supports only one value, but multiple values "
                    "'%(values)s' are provided")
            raise exception.ValidationError(msg % {"rule": self,
                            "operator": self.operator,
                            'values': ",".join(self.values)})


@register('simple_filter_expr_multi')
class SimpleFilterExprMulti(Filter):

    SUPPORTED_OPERATORS = SUPPORTED_OP_MULTI

    def __call__(self, target):
        """Run through this filter and maps it to the database model

        This filter is exactly same as SimpleFilterExprOne, except
        it supports different operators like 'in'|'nin'|'cont'|'ncont'
        which contains more than one value in the list.

        :returns
            A dict containing list of filter-specs required by
            sqlalchemy-filter.
        Example::
        operator=in, attribute=softwareImages/size, and value=[10,20]',
        then it would be mapped to following expression.
            {
                'field': 'size', -> Mapped to the version field
                'model': 'Foo', -> Mapped to database model
                'value': [10,20], -> Value to be used for filtering
                                     records
                'op': 'in', -> Attribute equal to one of the values in the
                               list ("in set" relationship)
            },
        """
        self._validate_operators()
        self._validate_attribute_name(target)
        self._validate_data_type(target)
        self._handle_values(target)

        return self.generate_expression(target, multiple_values=True)
