import itertools
import math

from django.contrib.auth import get_user_model
from django.db import models
from django.db.models import F
from django.urls import reverse
from django.utils import timezone

from .conf import settings
from .exceptions import DuplicateDocumentNameError, DuplicateFolderNameError
from .hooks import hookset
from .managers import DocumentQuerySet, FolderManager, FolderQuerySet


def uuid_filename(instance, filename):
    return hookset.file_upload_to(instance, filename)


class Folder(models.Model):

    name = models.CharField(max_length=140)
    parent = models.ForeignKey("self", null=True, blank=True, on_delete=models.CASCADE)
    author = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="+", on_delete=models.CASCADE)
    created = models.DateTimeField(default=timezone.now)
    modified = models.DateTimeField(default=timezone.now)
    modified_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="+", on_delete=models.CASCADE)

    objects = FolderManager.from_queryset(FolderQuerySet)()

    kind = "folder"
    icon = "folder-open"
    shared = None

    @classmethod
    def shared_user_model(cls):
        return FolderSharedUser

    @classmethod
    def already_exists(cls, name, parent=None):
        return cls.objects.filter(name=name, parent=parent).exists()

    def __str__(self):
        return self.name

    def save(self, **kwargs):
        if not self.pk and Folder.already_exists(self.name, self.parent):
            raise DuplicateFolderNameError(f"{self.name} already exists in this folder.")
        self.touch(self.author, commit=False)
        super().save(**kwargs)

    def get_absolute_url(self):
        return reverse("pinax_documents:folder_detail", args=[self.pk])

    def unique_id(self):
        return "f-%d" % self.id

    def members(self, **kwargs):
        return Folder.objects.members(self, **kwargs)

    def touch(self, user, commit=True):
        self.modified = timezone.now()
        self.modified_by = user
        if commit:
            if self.parent:
                self.parent.touch(user)
            self.save()

    @property
    def size(self):
        """
        Return size of this folder.
        """
        return sum([m.size for m in self.members(direct=False) if m.kind == "document"])

    def breadcrumbs(self):
        """
        Produces a list of ancestors (excluding self).
        """
        crumbs = []
        if self.parent:
            crumbs.extend(self.parent.breadcrumbs())
            crumbs.append(self.parent)
        return crumbs

    def shared_queryset(self):
        """
        Returns queryset of this folder mapped into the shared user model.
        The queryset should only consist of zero or one instances (aka shared
        or not shared.) This method is mostly used for convenience.
        """
        model = self.shared_user_model()
        return model._default_manager.filter(**{model.obj_attr: self})

    @property
    def shared(self):
        """
        Determines if self is shared. This checks the denormalization and
        does not return whether self SHOULD be shared (based on parents.)
        """
        return self.shared_queryset().exists()

    def shared_ui(self):
        """
        Returns boolean based on whether self should show any shared UI.
        """
        return self.parent_id is None and self.shared

    def shared_with(self, user=None):
        """
        Returns a User queryset of users shared on this folder, or, if user
        is given optimizes the check and returns boolean.
        """
        User = get_user_model()
        qs = self.shared_queryset()
        if user is not None:
            return qs.filter(user=user).exists()
        if not qs.exists():
            return User.objects.none()
        return User.objects.filter(pk__in=qs.values("user"))

    def shared_parent(self):
        """
        Returns the folder object that is the shared parent (the root of
        a shared folder hierarchy) or None if there is no shared parent.
        """
        root = self
        a, b = itertools.tee(reversed(self.breadcrumbs()))
        next(b, None)
        for folder, parent in itertools.zip_longest(a, b):
            if folder.shared:
                root = folder
            if parent is None or not parent.shared:
                break
        return root

    def can_share(self, user):
        """
        Ensures folder is top-level and `user` is the author.
        """
        return hookset.can_share_folder(user, self)

    def share(self, users):
        """
        Ensures self is shared with given users (can accept users who are
        already shared on self).
        """
        users = [u for u in users if not self.shared_with(user=u)]
        if users:
            members = [self] + self.members(direct=False)
            FM, DM = self.shared_user_model(), Document.shared_user_model()
            fm, dm = [], []
            for member, user in itertools.product(members, users):
                if user.pk == member.author_id:
                    continue
                if isinstance(member, Folder):
                    fm.append(FM(**{FM.obj_attr: member, "user": user}))
                if isinstance(member, Document):
                    dm.append(DM(**{DM.obj_attr: member, "user": user}))
            FM._default_manager.bulk_create(fm)
            DM._default_manager.bulk_create(dm)

    def delete_url(self):
        return reverse(
            "pinax_documents:folder_delete",
            args=[self.pk]
        )


class Document(models.Model):

    name = models.CharField(max_length=255)
    folder = models.ForeignKey(Folder, null=True, blank=True, on_delete=models.CASCADE)
    author = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="+", on_delete=models.CASCADE)
    created = models.DateTimeField(default=timezone.now)
    modified = models.DateTimeField(default=timezone.now)
    modified_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="+", on_delete=models.CASCADE)
    file = models.FileField(upload_to=uuid_filename)
    original_filename = models.CharField(max_length=500)

    objects = DocumentQuerySet.as_manager()

    kind = "document"
    icon = "file"
    shared = None

    def delete(self, *args, **kwargs):
        bytes_to_free = self.size
        super().delete(*args, **kwargs)
        storage_qs = UserStorage.objects.filter(pk=self.author.storage.pk)
        storage_qs.update(bytes_used=F("bytes_used") - bytes_to_free)

    @classmethod
    def shared_user_model(cls):
        return DocumentSharedUser

    @classmethod
    def already_exists(cls, name, folder=None):
        return cls.objects.filter(name=name, folder=folder).exists()

    def __str__(self):
        return self.name

    def save(self, **kwargs):
        if not self.pk and Document.already_exists(self.name, self.folder):
            raise DuplicateDocumentNameError(f"{self.name} already exists in this folder.")
        self.touch(self.author, commit=False)
        super().save(**kwargs)

    def get_absolute_url(self):
        return reverse("pinax_documents:document_detail", args=[self.pk])

    def unique_id(self):
        return "d-%d" % self.id

    def touch(self, user, commit=True):
        self.modified = timezone.now()
        self.modified_by = user
        if commit:
            if self.folder:
                self.folder.touch(user)
            self.save()

    @property
    def size(self):
        return self.file.size

    def breadcrumbs(self):
        crumbs = []
        if self.folder:
            crumbs.extend(self.folder.breadcrumbs())
            crumbs.append(self.folder)
        return crumbs

    def shared_queryset(self):
        """
        Returns queryset of this folder mapped into the shared user model.
        The queryset should only consist of zero or one instances (aka shared
        or not shared.) This method is mostly used for convenience.
        """
        model = self.shared_user_model()
        return model._default_manager.filter(**{model.obj_attr: self})

    @property
    def shared(self):
        """
        Determines if self is shared. This checks the denormalization and
        does not return whether self SHOULD be shared (based on parents.)
        """
        return self.shared_queryset().exists()

    def shared_ui(self):
        return False

    def shared_with(self, user=None):
        """
        Returns a User queryset of users shared on this folder, or, if user
        is given optimizes the check and returns boolean.
        """
        User = get_user_model()
        qs = self.shared_queryset()
        if user is not None:
            return qs.filter(user=user).exists()
        if not qs.exists():
            return User.objects.none()
        return User.objects.filter(pk__in=qs.values("user"))

    def share(self, users):
        users = [u for u in users if not self.shared_with(user=u)]
        if users:
            model = self.shared_user_model()
            objs = []
            for user in users:
                objs.append(self.shared_user_model()(**{model.obj_attr: self, "user": user}))
            model._default_manager.bulk_create(objs)

    def download_url(self):
        return reverse(
            "pinax_documents:document_download",
            args=[self.pk]
        )

    def delete_url(self):
        return reverse(
            "pinax_documents:document_delete",
            args=[self.pk]
        )


class MemberSharedUser(models.Model):

    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    # @@@ privileges

    class Meta:
        abstract = True

    @classmethod
    def for_user(cls, user):
        qs = cls._default_manager.filter(user=user)
        return qs.values_list(cls.obj_attr, flat=True)


class FolderSharedUser(MemberSharedUser):

    folder = models.ForeignKey(Folder, on_delete=models.CASCADE)
    obj_attr = "folder"

    class Meta:
        unique_together = [("folder", "user")]


class DocumentSharedUser(MemberSharedUser):

    document = models.ForeignKey(Document, on_delete=models.CASCADE)
    obj_attr = "document"

    class Meta:
        unique_together = [("document", "user")]


class UserStorage(models.Model):

    user = models.OneToOneField(settings.AUTH_USER_MODEL, related_name="storage", on_delete=models.CASCADE)
    bytes_used = models.BigIntegerField(default=0)
    bytes_total = models.BigIntegerField(default=0)

    @property
    def percentage(self):
        return int(math.ceil((float(self.bytes_used) / self.bytes_total) * 100))

    @property
    def color(self):
        return hookset.storage_color(self)