import operator
import unicodedata
import uuid
from functools import reduce

from django.conf import settings
from django.contrib.auth.models import AbstractBaseUser, UserManager
from django.contrib.auth.validators import UnicodeUsernameValidator
from django.db import models
from django.db.models.signals import pre_save
from django.dispatch import receiver
from django.utils import timezone, translation
from django.utils.translation import gettext_lazy as _
from localized_fields.fields import LocalizedCharField, LocalizedTextField
from tree_queries.models import TreeNode, TreeQuerySet


def make_uuid():
    """Return a new random UUID value.

    This indirection is done for testing purposes, so test code can mock
    uuid.uuid4(). If we wouldn't do this, then the models would have a direct
    reference that doesn't get mocked away.

    We can't replace it with a lambda because Django Migrations can't handle them.
    """
    return uuid.uuid4()


def get_language_code():
    return settings.LANGUAGE_CODE


class BaseModel(models.Model):
    created_at = models.DateTimeField(auto_now_add=True, db_index=True)
    modified_at = models.DateTimeField(auto_now=True, db_index=True)
    created_by_user = models.ForeignKey("User", null=True, on_delete=models.SET_NULL)
    metainfo = models.JSONField(_("metainfo"), default=dict)

    class Meta:
        abstract = True


class UUIDModel(BaseModel):
    """
    Models which use uuid as primary key.

    Defined as emeis default
    """

    id = models.UUIDField(primary_key=True, default=make_uuid, editable=False)

    class Meta:
        abstract = True


class SlugModel(BaseModel):
    """
    Models which use a slug as primary key.

    Defined as Emeis default for configuration so it is possible
    to merge between developer and user configuration.
    """

    slug = models.SlugField(max_length=255, primary_key=True)

    class Meta:
        abstract = True


class User(UUIDModel, AbstractBaseUser):
    username_validator = UnicodeUsernameValidator()

    username = models.CharField(
        _("username"),
        max_length=255,
        unique=True,
        help_text=_(
            "Required. 255 characters or fewer. Letters, digits and @/./+/-/_ only."
        ),
        validators=[username_validator],
        error_messages={"unique": _("A user with that username already exists.")},
    )
    first_name = models.CharField(
        _("first name"), max_length=255, blank=True, null=True
    )
    last_name = models.CharField(_("last name"), max_length=255, blank=True, null=True)
    email = models.EmailField(_("email address"), blank=True, null=True)
    phone = models.CharField(_("phone"), max_length=100, blank=True, null=True)
    language = models.CharField(_("language"), max_length=2, default=get_language_code)
    address = models.CharField(_("address"), max_length=255, blank=True, null=True)
    city = LocalizedCharField(_("city"), max_length=255, blank=True, null=True)
    zip = models.CharField(_("zip"), max_length=10, blank=True, null=True)
    is_active = models.BooleanField(
        _("active"),
        default=True,
        help_text=_("Designates whether this user should be treated as active."),
    )
    date_joined = models.DateTimeField(_("date joined"), default=timezone.now)

    EMAIL_FIELD = "email"
    USERNAME_FIELD = "username"
    REQUIRED_FIELDS = ["email"]

    objects = UserManager()

    def __str__(self):
        return "%s (username=%s)" % (type(self).__name__, self.username)

    class Meta:
        verbose_name = _("user")
        verbose_name_plural = _("users")

    def clean(self):
        super().clean()
        setattr(self, self.USERNAME_FIELD, self.normalize_username(self.get_username()))

        self.email = UserManager.normalize_email(self.email)

    def get_full_name(self):
        """Return the first_name plus the last_name, with a space in between."""
        full_name = "%s %s" % (self.first_name, self.last_name)
        return full_name.strip()

    def get_username(self):
        """Return the username for this User."""
        return getattr(self, self.USERNAME_FIELD)

    def natural_key(self):
        return self.get_username()

    @classmethod
    def get_email_field_name(cls):
        return "email"

    @classmethod
    def normalize_username(cls, username):
        return (
            unicodedata.normalize("NFKC", username)
            if isinstance(username, str)
            else username
        )

    @property
    def is_anonymous(self):
        """
        Return False.

        This is a way of comparing User objects to anonymous users.
        """
        return False

    @property
    def is_authenticated(self):
        """
        Return True.

        This is a way to tell if the user has been authenticated in templates.
        """
        return True


class ScopeQuerySet(TreeQuerySet):
    # django-tree-queries sadly does not (yet?) support ancestors query
    # for QS - only for single nodes. So we're providing all_descendants()
    # and all_ancestors() queryset methods.

    def all_descendants(self, include_self=False):
        """Return a QS that contains all descendants of the given QS.

        This is a workaround for django-tree-queries, which currently does
        not support this query (it can only do it on single nodes).

        This is in contrast to .descendants(), which can only give the descendants
        of one model instance.
        """
        descendants_q = reduce(
            operator.or_,
            [
                models.Q(
                    pk__in=entry.descendants(include_self=include_self).values("pk")
                )
                for entry in self
            ],
            models.Q(),
        )
        return self.model.objects.filter(descendants_q)

    def all_ancestors(self, include_self=False):
        """Return a QS that contains all ancestors of the given QS.

        This is a workaround for django-tree-queries, which currently does
        not support this query (it can only do it on single nodes).

        This is in contrast to .ancestors(), which can only give the ancestors
        of one model instance.
        """

        descendants_q = reduce(
            operator.or_,
            [
                models.Q(pk__in=entry.ancestors(include_self=include_self).values("pk"))
                for entry in self
            ],
            models.Q(),
        )
        return self.model.objects.filter(descendants_q)

    def all_roots(self):
        return Scope.objects.all().filter(
            pk__in=[scope.ancestors(include_self=True).first().pk for scope in self]
        )


class Scope(TreeNode, UUIDModel):
    name = LocalizedCharField(_("scope name"), blank=False, null=False, required=False)

    full_name = LocalizedCharField(
        _("scope name"), blank=True, null=True, required=False
    )

    description = LocalizedTextField(
        _("scope description"), null=True, blank=True, required=False
    )
    is_active = models.BooleanField(default=True)

    objects = ScopeQuerySet.as_manager(with_tree_fields=True)

    def get_root(self):
        return self.ancestors(include_self=True).first()

    def save(self, *args, **kwargs):
        # django-tree-queries does validation in TreeNode.clean(), which is not
        # called by DRF (only by django forms), so we have to do this here
        self.clean()
        return super().save(*args, **kwargs)

    def __str__(self):
        return f"{type(self).__name__} ({self.full_name}, pk={self.pk})"

    class Meta:
        ordering = ["name"]


@receiver(pre_save, sender=Scope)
def set_full_name(instance, sender, **kwargs):
    if kwargs.get("raw"):  # pragma: no cover
        # Raw is set while loading fixtures. In those
        # cases we don't want to mess with data that
        # may not be there yet
        return
    sep = "\u00bb"

    languages = [lang for lang, _ in settings.LANGUAGES]

    forced_lang = settings.EMEIS_FORCE_MODEL_LOCALE.get("scope", None)

    if forced_lang:
        # If scope is forced monolingual, do not fill non-forced language fields
        languages = [forced_lang]

    for lang in languages:
        with translation.override(lang):
            instance.full_name[lang] = str(instance.name)

    parent = instance.parent
    while parent:
        for lang in languages:
            with translation.override(lang):
                new_fullname = f"{parent.name} {sep} {instance.full_name[lang]}"
                instance.full_name[lang] = new_fullname
        parent = parent.parent

    if forced_lang:
        # Ensure only the "forced" language full_name is set, and nothing else
        full_name = instance.full_name[forced_lang]
        instance.full_name.clear()
        instance.full_name[forced_lang] = full_name

    # Force update of all children (recursively)
    for child in instance.children.all():
        # save() triggers the set_full_name signal handler, which will
        # recurse all the way down, updating the full_name
        child.save()


class Role(SlugModel):
    name = LocalizedCharField(_("role name"), blank=False, null=False, required=False)
    description = LocalizedTextField(
        _("role description"), null=True, blank=True, required=False
    )
    permissions = models.ManyToManyField("Permission", related_name="roles")

    def __str__(self):
        return f"{type(self).__name__} ({self.pk})"

    class Meta:
        ordering = ["slug"]


class Permission(SlugModel):
    name = LocalizedCharField(
        _("permission name"), blank=False, null=False, required=False
    )
    description = LocalizedTextField(
        _("permission description"), null=True, blank=True, required=False
    )


class ACL(UUIDModel):
    user = models.ForeignKey("User", on_delete=models.CASCADE, related_name="acls")
    scope = models.ForeignKey("Scope", on_delete=models.CASCADE, related_name="acls")
    role = models.ForeignKey("Role", on_delete=models.CASCADE, related_name="acls")

    def __str__(self):
        return f"{type(self).__name__} (username={self.user.username}, scope=self.scope, role={self.role.pk})"

    class Meta:
        unique_together = ["user", "scope", "role"]
