#!/usr/bin/env python
# -*- coding: utf-8 -*-
import logging
from collections import OrderedDict
from functools import wraps

from django.core import checks
from django.core.exceptions import FieldDoesNotExist
from django.db.models import ForeignObjectRel
from django.db.models.fields.related import ForeignObject
from django.db.models.fields.related_descriptors import (
    ForwardManyToOneDescriptor,
    ReverseOneToOneDescriptor,
)
from django.db.models.sql.where import AND, WhereNode


def get_cached_value(instance, descriptor, default=None):
    return descriptor.field.get_cached_value(instance, default=default)


def set_cached_value_by_descriptor(instance, descriptor, value):
    descriptor.field.set_cached_value(instance, value)


def set_cached_value_by_field(instance, field, value):
    field.set_cached_value(instance, value)


logger = logging.getLogger(__name__)


# TODO: Figure out if nullable fields is a proper error
class CompositeForwardManyToOneDescriptor(ForwardManyToOneDescriptor):
    def __set__(self, instance, value):
        if value is not None or not self.field.nullable_fields:
            super().__set__(instance, value)
        else:
            # we set only the asked fields to None, not all field as the default ForwardManyToOneDescriptor will

            # ### taken from original ForwardManyToOneDescriptor
            # Look up the previously-related object, which may still be available
            # since we've not yet cleared out the related field.
            # Use the cache directly, instead of the accessor; if we haven't
            # populated the cache, then we don't care - we're only accessing
            # the object to invalidate the accessor cache, so there's no
            # need to populate the cache just to expire it again.
            related = get_cached_value(instance, self, None)

            # If we've got an old related object, we need to clear out its
            # cache. This cache also might not exist if the related object
            # hasn't been accessed yet.
            if related is not None:
                related_field = self.field.remote_field
                set_cached_value_by_field(related, related_field, None)

            # ##### only original part
            for lh_field_name, none_value in self.field.nullable_fields.items():
                setattr(instance, lh_field_name, none_value)

            # Set the related instance cache used by __get__ to avoid a SQL query
            # when accessing the attribute we just set.
            set_cached_value_by_descriptor(instance, self, None)


class CompositeForeignKey(ForeignObject):
    requires_unique_target = False
    auto_created = False
    empty_strings_allowed = False

    many_to_many = False
    many_to_one = False
    one_to_many = True
    one_to_one = False

    rel_class = ForeignObjectRel

    def __init__(self, to, **kwargs):
        """
        create the ForeignObject, but use the to_fields as a dict which will later used as form_fields and to_fields
        """
        to_fields = kwargs["to_fields"]
        self.null_if_equal = kwargs.pop("null_if_equal", [])
        nullable_fields = kwargs.pop("nullable_fields", {})
        if not isinstance(nullable_fields, dict):
            nullable_fields = {v: None for v in nullable_fields}
        self.nullable_fields = nullable_fields

        # a list of tuple : (fieldnaem, value) . if fielname = value, then the field react as if fieldnaem_id = None
        self._raw_fields = self.compute_to_fields(to_fields)
        # hiro nakamura should have said «very bad guy. you are vilain»
        if "on_delete" in kwargs:
            kwargs["on_delete"] = self.override_on_delete(kwargs["on_delete"])

        kwargs["to_fields"], kwargs["from_fields"] = zip(
            *((k, v.value) for k, v in self._raw_fields.items() if v.is_local_field), strict=False
        )
        super().__init__(to, **kwargs)

    def override_on_delete(self, original):
        @wraps(original)
        def wrapper(collector, field, sub_objs, using):
            res = original(collector, field, sub_objs, using)
            # we make something nasty : we update the collector to
            # skip the local field which does not have a dbcolumn
            try:
                del collector.field_updates[self.model][(self, None)]
            except KeyError:
                pass
            return res

        wrapper._original_fn = original

        return wrapper

    def check(self, **kwargs):
        errors = super().check(**kwargs)
        errors.extend(self._check_null_with_nullifequal())
        errors.extend(self._check_nullifequal_fields_exists())
        errors.extend(self._check_to_fields_local_valide())
        errors.extend(self._check_to_fields_remote_valide())
        errors.extend(self._check_recursion_field_dependecy())
        errors.extend(self._check_bad_order_fields())
        return errors

    def _check_bad_order_fields(self):
        res = []
        try:
            dependents = list(self.local_related_fields)
        except FieldDoesNotExist:
            return []  # the errors shall be raised befor by _check_recursion_field_dependecy

        for field in self.model._meta.get_fields():
            try:
                dependents.remove(field)
            except ValueError:
                pass
            if field == self:
                if dependents:
                    # we met the current fields, but all dependent fields is not
                    # passed befor : we will have a problem in the init of some objects
                    # where the rest of dependents fields will override the
                    # values set by the current one (see Model.__init__)
                    res.append(
                        checks.Error(
                            "the field %s depend on the fields %s which is defined after. define them befor %s"
                            % (self.name, ",".join(f.name for f in dependents), self.name),
                            hint=None,
                            obj=self,
                            id="compositefk.E006",
                        )
                    )
                break
        return res

    def _check_recursion_field_dependecy(self):
        res = []
        for local_field in self._raw_fields.values():
            try:
                f = self.model._meta.get_field(local_field.value)
                if isinstance(f, CompositeForeignKey):
                    res.append(
                        checks.Error(
                            "the field %s depend on the field %s which is another CompositeForeignKey"
                            % (self.name, local_field),
                            hint=None,
                            obj=self,
                            id="compositefk.E005",
                        )
                    )
            except FieldDoesNotExist:
                pass  # _check_to_fields_local_valide already raise errors for this
        return res

    def _check_to_fields_local_valide(self):
        res = []
        for local_field in self._raw_fields.values():
            if isinstance(local_field, LocalFieldValue):
                try:
                    self.model._meta.get_field(local_field.value)
                except FieldDoesNotExist:
                    res.append(
                        checks.Error(
                            "the field %s does not exists on the model %s" % (local_field, self.model),
                            hint=None,
                            obj=self,
                            id="compositefk.E003",
                        )
                    )
        return res

    def _check_to_fields_remote_valide(self):
        res = []
        for remote_field in self._raw_fields.keys():
            try:
                self.related_model._meta.get_field(remote_field)
            except FieldDoesNotExist:
                res.append(
                    checks.Error(
                        "the field %s does not exists on the model %s" % (remote_field, self.model),
                        hint=None,
                        obj=self,
                        id="compositefk.E004",
                    )
                )
        return res

    def _check_null_with_nullifequal(self):
        if self.null_if_equal and not self.null:
            return [
                checks.Error(
                    "you must set null=True to field %s.%s if null_if_equal is given"
                    % (self.model.__class__.__name__, self.name),
                    hint=None,
                    obj=self,
                    id="compositefk.E001",
                )
            ]
        return []

    def _check_nullifequal_fields_exists(self):
        res = []
        for field_name, _ in self.null_if_equal:
            try:
                self.model._meta.get_field(field_name)
            except FieldDoesNotExist:
                res.append(
                    checks.Error(
                        "the field %s does not exists on the model %s" % (field_name, self.model),
                        hint=None,
                        obj=self,
                        id="compositefk.E002",
                    )
                )
        return res

    def deconstruct(self):
        name, path, args, kwargs = super(CompositeForeignKey, self).deconstruct()
        del kwargs["from_fields"]
        if "on_delete" in kwargs:
            kwargs["on_delete"] = kwargs["on_delete"]._original_fn
        kwargs["to_fields"] = self._raw_fields
        kwargs["null_if_equal"] = self.null_if_equal
        return name, path, args, kwargs

    def get_extra_descriptor_filter(self, instance):
        return {k: v.value for k, v in self._raw_fields.items() if isinstance(v, RawFieldValue)}

    def get_extra_restriction(self, alias, related_alias):
        constraint = WhereNode(connector=AND)
        for remote, local in self._raw_fields.items():
            lookup = local.get_lookup(self, self.related_model._meta.get_field(remote), alias)
            if lookup:
                constraint.add(lookup, AND)
        if constraint.children:
            return constraint
        else:
            return None

    def compute_to_fields(self, to_fields):
        """
        compute the to_fields parameterse to make it uniformly a dict of CompositePart
        :param set[unicode]|dict[unicode, unicode] to_fields: the list/dict of fields to match
        :return: the well formated to_field containing only subclasses of CompositePart
        :rtype: dict[str, CompositePart]
        """
        # for problem in trim_join, we must try to give the fields in a consistent order with others models...
        # see #26515 at  https://code.djangoproject.com/ticket/26515

        return OrderedDict(
            (k, (v if isinstance(v, CompositePart) else LocalFieldValue(v)))
            for k, v in (to_fields.items() if isinstance(to_fields, dict) else zip(to_fields, to_fields, strict=False))
        )

    def db_type(self, connection):
        # A CompositeForeignKey don't have a column in the database
        # so return None.
        return None

    def db_parameters(self, connection):
        return {"type": None, "check": None}

    def contribute_to_class(self, cls, name, **kwargs):
        super().contribute_to_class(cls, name, **kwargs)
        setattr(cls, self.name, CompositeForwardManyToOneDescriptor(self))

    def get_instance_value_for_fields(self, instance, fields):
        # we override this method to provide the feathur of converting
        # some special values of teh composite local fields into a
        # None pointing field.
        # ie, if company is '   ' and it mean that the current field
        # point to nothing (as if it was None) => we transform this
        # '   ' into a true None to let django das as if it was None
        res = super(CompositeForeignKey, self).get_instance_value_for_fields(instance, fields)
        if self.null_if_equal:
            for field_name, exception_value in self.null_if_equal:
                val = getattr(instance, field_name)
                if val == exception_value:
                    # we have field_name that is equal to the bad value
                    # currently, it is enouth since the django implementation check at first
                    # if there is a None in the result
                    return (None,)
        return res


class CompositeOneToOneField(CompositeForeignKey):
    # Field flags
    many_to_many = False
    many_to_one = False
    one_to_many = False
    one_to_one = True

    related_accessor_class = ReverseOneToOneDescriptor

    description = "One-to-one relationship"

    def __init__(self, to, **kwargs):
        kwargs["unique"] = True
        super().__init__(to, **kwargs)
        self.remote_field.multiple = False

    def deconstruct(self):
        name, path, args, kwargs = super().deconstruct()
        if "unique" in kwargs:
            del kwargs["unique"]
        return name, path, args, kwargs


class CompositePart(object):
    is_local_field = True

    def __init__(self, value):
        self.value = value

    def deconstruct(self):
        module_name = self.__module__
        name = self.__class__.__name__
        return ("%s.%s" % (module_name, name), (self.value,), {})

    def __repr__(self):
        return "%s(%r)" % (self.__class__.__name__, self.value)

    def __eq__(self, other):
        if self.__class__ != other.__class__:
            return False
        return self.value == other.value

    def get_lookup(self, main_field, for_remote, alias):
        """
        create a fake field for the lookup capability
        :param CompositeForeignKey main_field: the local fk
        :param Field for_remote: the remote field to match
        :return:
        """


class RawFieldValue(CompositePart):
    """
    represent a raw value for  a field.
    """

    is_local_field = False

    def get_lookup(self, main_field, for_remote, alias):
        """
        create a fake field for the lookup capability
        :param CompositeForeignKey main_field: the local fk
        :param Field for_remote: the remote field to match
        :return:
        """
        lookup_class = for_remote.get_lookup("exact")
        return lookup_class(for_remote.get_col(alias), self.value)


class FunctionBasedFieldValue(RawFieldValue):
    def __init__(self, func):
        self._func = func

    def deconstruct(self):
        module_name = self.__module__
        name = self.__class__.__name__
        return ("%s.%s" % (module_name, name), (self._func,), {})

    def __eq__(self, other):
        if self.__class__ != other.__class__:
            return False
        return self._func == other._func

    @property
    def value(self):
        return self._func()

    @value.setter
    def value(self):
        pass


class LocalFieldValue(CompositePart):
    """
    implicitly used, represent the value of a local field
    """

    is_local_field = True
