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

"""Helper to create sqlalchemy filters according to filter querystring parameter"""

from sqlalchemy import and_, or_, not_

from flask_rest_jsonapi.exceptions import InvalidFilters
from flask_rest_jsonapi.schema import get_relationships, get_nested_fields, get_model_field


def create_filters(model, filter_info, resource):
    """Apply filters from filters information to base query

    :param DeclarativeMeta model: the model of the node
    :param dict filter_info: current node filter information
    :param Resource resource: the resource
    """
    filters = []
    for filter_ in filter_info:
        filters.append(Node(model, filter_, resource, resource.schema).resolve())

    return filters


class Node(object):
    """Helper to recursively create filters with sqlalchemy according to filter querystring parameter"""

    def __init__(self, model, filter_, resource, schema):
        """Initialize an instance of a filter node

        :param Model model: an sqlalchemy model
        :param dict filter_: filters information of the current node and deeper nodes
        :param Resource resource: the base resource to apply filters on
        :param Schema schema: the serializer of the resource
        """
        self.model = model
        self.filter_ = filter_
        self.resource = resource
        self.schema = schema

    def resolve(self):
        """Create filter for a particular node of the filter tree"""
        if 'or' not in self.filter_ and 'and' not in self.filter_ and 'not' not in self.filter_:
            value = self.value

            if isinstance(value, dict):
                value = Node(self.related_model, value, self.resource, self.related_schema).resolve()

            if '__' in self.filter_.get('name', ''):
                value = {self.filter_['name'].split('__')[1]: value}

            if isinstance(value, dict):
                return getattr(self.column, self.operator)(**value)
            else:
                return getattr(self.column, self.operator)(value)

        if 'or' in self.filter_:
            return or_(Node(self.model, filt, self.resource, self.schema).resolve() for filt in self.filter_['or'])
        if 'and' in self.filter_:
            return and_(Node(self.model, filt, self.resource, self.schema).resolve() for filt in self.filter_['and'])
        if 'not' in self.filter_:
            return not_(Node(self.model, self.filter_['not'], self.resource, self.schema).resolve())

    @property
    def name(self):
        """Return the name of the node or raise a BadRequest exception

        :return str: the name of the field to filter on
        """
        name = self.filter_.get('name')

        if name is None:
            raise InvalidFilters("Can't find name of a filter")

        if '__' in name:
            name = name.split('__')[0]

        if name not in self.schema._declared_fields:
            raise InvalidFilters("{} has no attribute {}".format(self.schema.__name__, name))

        return name

    @property
    def op(self):
        """Return the operator of the node

        :return str: the operator to use in the filter
        """
        try:
            return self.filter_['op']
        except KeyError:
            raise InvalidFilters("Can't find op of a filter")

    @property
    def column(self):
        """Get the column object

        :param DeclarativeMeta model: the model
        :param str field: the field
        :return InstrumentedAttribute: the column to filter on
        """
        field = self.name

        model_field = get_model_field(self.schema, field)

        try:
            return getattr(self.model, model_field)
        except AttributeError:
            raise InvalidFilters("{} has no attribute {}".format(self.model.__name__, model_field))

    @property
    def operator(self):
        """Get the function operator from his name

        :return callable: a callable to make operation on a column
        """
        operators = (self.op, self.op + '_', '__' + self.op + '__')

        for op in operators:
            if hasattr(self.column, op):
                return op

        raise InvalidFilters("{} has no operator {}".format(self.column.key, self.op))

    @property
    def value(self):
        """Get the value to filter on

        :return: the value to filter on
        """
        if self.filter_.get('field') is not None:
            try:
                result = getattr(self.model, self.filter_['field'])
            except AttributeError:
                raise InvalidFilters("{} has no attribute {}".format(self.model.__name__, self.filter_['field']))
            else:
                return result
        else:
            if 'val' not in self.filter_:
                raise InvalidFilters("Can't find value or field in a filter")

            return self.filter_['val']

    @property
    def related_model(self):
        """Get the related model of a related (relationship or nested) field

        :return DeclarativeMeta: the related model
        """
        related_field_name = self.name

        related_fields = get_relationships(self.schema) + get_nested_fields(self.schema)
        if related_field_name not in related_fields:
            raise InvalidFilters("{} has no relationship or nested attribute {}".format(self.schema.__name__, related_field_name))

        return getattr(self.model, get_model_field(self.schema, related_field_name)).property.mapper.class_

    @property
    def related_schema(self):
        """Get the related schema of a related (relationship or nested) field

        :return Schema: the related schema
        """
        related_field_name = self.name
        related_fields = get_relationships(self.schema) + get_nested_fields(self.schema)
        if related_field_name not in related_fields:
            raise InvalidFilters("{} has no relationship or nested attribute {}".format(self.schema.__name__, related_field_name))

        return self.schema._declared_fields[related_field_name].schema.__class__
