# -*- coding: utf-8 -*-
"""
Copyright 2013-2014 Olivier Cortès <oc@1flow.io>.

This file is part of the 1flow project.

1flow is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version.

1flow is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public
License along with 1flow.  If not, see http://www.gnu.org/licenses/

"""

import uuid
import logging

from django.db import models
from django.db.models.signals import pre_save
from django.utils.translation import ugettext_lazy as _
from django.utils.text import slugify
from django.contrib.auth.models import Group
from django.contrib.contenttypes import generic

from mptt.models import MPTTModel, TreeForeignKey

from sparks.django.models.mixins import DiffMixin

from oneflow.base.fields import IntRedisDescriptor
from oneflow.base.utils import register_task_method

from common import (
    FOLDER_ANONYMOUS_MEMBERSHIP_TYPES,
    PROPOSAL_ACTION_TYPES,
    lowername,
    DjangoUser as User,
)


LOGGER = logging.getLogger(__name__)


__all__ = [
    'Folder',

    'folder_all_items_count_default',
    'folder_starred_items_count_default',
    'folder_unread_items_count_default',
    'folder_archived_items_count_default',
    'folder_bookmarked_items_count_default',
]


def qs_order_by_lower_name(queryset, field=None):
    """ Order a QuerySet by lowercase(field); field is “name” by default. """

    return queryset.extra(
        select={'_lower_order_by_field_': 'lower({0})'.format(field or 'name')}
    ).order_by('_lower_order_by_field_')


def get_folder_image_upload_path(instance, filename):

    if not filename.strip():
        filename = uuid.uuid4()

    # The filename will be used in a shell command later. In case the
    # user/admin forgets the '"' in the configuration, avoid problems.
    filename = filename.replace(u' ', u'_')

    if instance:
        return 'user/{0}/folder/{1}/images/{2}'.format(
            instance.user.username, instance.id, filename)

    return u'images/%Y/%m/%d/{0}'.format(filename)


# ——————————————————————————————————————————————————————————————— Redis Helpers


def folder_all_items_count_default(folder, *args, **kwargs):

    return folder.reads.all().count()


def folder_starred_items_count_default(folder, *args, **kwargs):

    return folder.reads.filter(is_starred=True).count()


def folder_unread_items_count_default(folder, *args, **kwargs):

    return folder.reads.filter(is_read=False).count()


def folder_archived_items_count_default(folder, *args, **kwargs):

    return folder.reads.filter(is_archived=True).count()


def folder_bookmarked_items_count_default(folder, *args, **kwargs):

    return folder.reads.filter(is_bookmarked=True).count()


# —————————————————————————————————————————————————————————————————————— Models


class Folder(MPTTModel, DiffMixin):

    """ A simple folder structure. """

    class Meta:
        app_label = 'core'
        verbose_name = _(u'Folder')
        verbose_name_plural = _(u'Folders')
        unique_together = ('name', 'user', 'parent', )

    user = models.ForeignKey(User, verbose_name=_(u'Owner'),
                             related_name='folders')

    name = models.CharField(verbose_name=_(u'Name'), max_length=255)
    slug = models.CharField(verbose_name=_(u'slug'), max_length=255,
                            null=True, blank=True)

    parent = TreeForeignKey('self', null=True, blank=True,
                            related_name='children')

    date_created = models.DateTimeField(auto_now_add=True, blank=True,
                                        verbose_name=_(u'Date created'))

    # Allow the user to also customize the visual of his/her subscription.
    image = models.ImageField(
        verbose_name=_(u'Image'), null=True, blank=True,
        upload_to=get_folder_image_upload_path, max_length=256,
        help_text=_(u'A custom image for the folder. Takes precedence over '
                    u'image_url if both are filled.'))

    image_url = models.URLField(
        verbose_name=_(u'Image URL'), null=True, blank=True, max_length=384,
        help_text=_(u'A full URL of an online image, if you prefer hosting '
                    u'it outside of 1flow.'))

    # ——————————————————————————————————————————————————— Shared folders fields

    is_shared = models.BooleanField(
        verbose_name=_(u'Share this folder'),
        default=False, blank=True
    )
    is_moderated = models.BooleanField(
        verbose_name=_(u'Moderate members proposals'),
        default=True, blank=True,
        help_text=_(u'Any feed addition/removal/change in this folder must '
                    u'be validated by the owner/creator or a folder staff '
                    u'member before beiing accepted.'))
    is_restricted = models.BooleanField(
        verbose_name=_(u'All content is restricted'),
        default=True, blank=True,
        help_text=_(u'In case un-authenticated users have access to this '
                    u'folder, marking it restricted will avoid them to see '
                    u'the folder items full content, but only excerpts '
                    u'or thumbnails instead. If a folder is restricted, '
                    u'this takes precedence over feed and item restricted '
                    u'state, and makes them restricted even if they are not.'))

    staff_group = models.OneToOneField(Group, null=True, blank=True,
                                       related_name='staff_folder')
    members_group = models.OneToOneField(Group, null=True, blank=True,
                                         related_name='members_folder')
    guests_group = models.OneToOneField(Group, null=True, blank=True,
                                        related_name='guests_folder')

    # Not used yet.
    # is_permissive = models.BooleanField(default=False, blank=True)

    anonymous_membership = models.IntegerField(
        verbose_name=_(u'Anonymous visitors have'),
        choices=FOLDER_ANONYMOUS_MEMBERSHIP_TYPES.get_choices(),
        default=FOLDER_ANONYMOUS_MEMBERSHIP_TYPES.NO_ACCESS,
        help_text=_(u'Define the access that un-authenticated users will '
                    u'have on this shared folder.'))

    # Moderation proposals to include / remove things in / from this folder.
    proposals = generic.GenericRelation(
        'Proposal',
        content_type_field='context_content_type',
        object_id_field='context_id'
    )

    # —————————————————————————————————————————————————————— Cached descriptors

    all_items_count = IntRedisDescriptor(
        attr_name='f.aa_c', default=folder_all_items_count_default,
        set_default=True)

    unread_items_count = IntRedisDescriptor(
        attr_name='f.ua_c', default=folder_unread_items_count_default,
        set_default=True)

    starred_items_count = IntRedisDescriptor(
        attr_name='f.sa_c', default=folder_starred_items_count_default,
        set_default=True)

    archived_items_count = IntRedisDescriptor(
        attr_name='f.ra_c',
        default=folder_archived_items_count_default,
        set_default=True, min_value=0)

    bookmarked_items_count = IntRedisDescriptor(
        attr_name='f.ba_c', default=folder_bookmarked_items_count_default,
        set_default=True)

    @property
    def read_items_count(self):
        """ Return all items count minus unread items count. """

        return self.all_items_count - self.unread_items_count

    # ————————————————————————————————————————————————————————— Django & Python

    def __unicode__(self):
        """ Wake up, pep257. That's just the unicode method. """

        return _(u'{0} (#{1}, level {2}) for user {3}{4}{5}').format(
            self.name, self.id, self.level, self.user,
            _(u', parent: {0} (#{1})').format(self.parent.name, self.parent.id)
            if self.parent else u'',
            _(u', children: {0}').format(u', '.join(
                _(u'{0} (#{1})').format(child.name, child.id)
                for child in self.children.all()))
            if self.children != [] else u'')

    # —————————————————————————————————————————————————————————————— Properties

    @property
    def is_root(self):
        """ Return True if current folder is a root for a user. """

        return self.name == u'__root__' and self.parent is None

    @property
    def has_content(self):

        # PERF: it's faster to test for children than query for subscriptions.
        return self.children or self.subscriptions

    @property
    def max_depth(self):

        return 4 \
            if self.user.preferences.selector.extended_folders_depth \
            else 2

    @property
    def children_tree(self):
        """ Return my children and their own, in tree order. """

        return self.get_descendants()

    @property
    def children_by_name(self):

        return qs_order_by_lower_name(self.children.all())

    # ——————————————————————————————————————————————————————————— Class methods

    @classmethod
    def get_root_for(cls, user):
        """ Get the root folder for a user. Might create it on the fly. """

        root, created = cls.objects.get_or_create(user=user,
                                                  name=u'__root__',
                                                  parent=None)

        return root

    @classmethod
    def add_folder_from_tag(cls, tag, user, parent=None):
        """ Return (folder, created) or raise an exception. """

        raise NotImplementedError("REVIEW for RELDB.")

        if len(tag.parents) == 1:
            parent_folder, _created = cls.add_folder_from_tag(tag.parents[0],
                                                              user)

        elif len(tag.parents) > 1:
            best_parent = tag.parents[0]

            # TODO: get the best tag from the LANG or whatever.
            #
            # for parent_tag in tag.parents:
            #    …

            parent_folder, _created = cls.add_folder_from_tag(best_parent,
                                                              user)

        else:
            parent_folder = None

        folder, created = cls.add_folder(tag.name, user, parent_folder)

        return folder, created

    @classmethod
    def add_folder(cls, name, user, parent=None, children=None):
        """ Return (folder, created) or raise an exception. """

        assert parent is None or isinstance(parent, cls)
        assert children is None or hasattr(children, '__iter__')

        if children is None:
            children = ()

        if parent is None:
            parent = cls.get_root_for(user)

        # Should we disable the name __root__ ?
        # In fact, this doesn't raise any problem.
        # There will always be a root with parent=None
        # Satisfy the user root folder query.
        # So let the user be free.
        #
        # elif name == u'__root__':
        #     raise RuntimeError('Name __root___ is reserved')

        folder, created = cls.objects.get_or_create(name=name,
                                                    user=user,
                                                    parent=parent)
        if created:
            parent.children.add(folder)

            if children:
                folder.children.add(*children)

            LOGGER.info(u'Created folder %s.', folder)

        return folder, created

    # ————————————————————————————————————————————————————————————————— Methods

    def compute_cached_descriptors(self, **kwargs):
        """ Do you guess? I guess yes. """

        self.all_items_count = \
            folder_all_items_count_default(self)

        self.unread_items_count = \
            folder_unread_items_count_default(self)

        self.starred_items_count = \
            folder_starred_items_count_default(self)

        self.archived_items_count = \
            folder_archived_items_count_default(self)

        self.bookmarked_items_count = \
            folder_bookmarked_items_count_default(self)

    def get_subfolders(self, current_level):

        raise NotImplementedError('Use MPTT for get_subfolders()')

        if current_level >= self.max_depth:
            return models.QuerySet()

        children = models.QuerySet(model=self.__class__)

        for child in sorted(self.children, key=lowername):
            children.append(child)
            children.extend(child.get_subfolders(current_level + 1))

        return children

    def purge(self, self_delete=True):
        """ Remove a folder and all its content (subfolders, subscriptions). """

        for child in self.children.all():

            # Don't bother multiple queries to delete children:
            # with MPTT, deleting us will also delete our children.
            child.purge(self_delete=False)

        for subscription in self.subscriptions.all():
            # Don't delete subscription if present in another folder, in case
            # user has activated the multi-folder subscriptions preference.
            if subscription.folders.count() == 1:
                subscription.delete()

        if self_delete:
            self.delete()

    # —————————————————————————————————————————— Proposals steps implementation

    def proposal_step_SUBMITTED(self, proposal):
        """ Notify reviewers of a new proposal. """


        return True

    def proposal_step_ACCEPTED(self, proposal):
        """ Execute a proposal on the current folder. """

        retval = False

        if proposal.action == PROPOSAL_ACTION_TYPES.ADD:

            # Add proposal.target to self.
            retval = True

        elif proposal.action == PROPOSAL_ACTION_TYPES.REMOVE:

            # Remove proposal.target from self.
            retval = True

        elif proposal.action == PROPOSAL_ACTION_TYPES.CHANGE:

            # Change proposal.target in self.
            # Note: proposal.target can be self.
            retval = True

        else:
            LOGGER.warning(u'Unhandled action %s for %s on %s',
                           proposal.action, proposal.target, self)

        return retval

# ——————————————————————————————————————————————————————————————— Tasks methods


register_task_method(Folder, Folder.purge, globals(), queue=u'background')


# ————————————————————————————————————————————————————————————————————— Signals


def folder_pre_save(instance, **kwargs):

    if not instance.slug:
        instance.slug = slugify(instance.name)

    if instance.is_shared and 'is_shared' in instance.changed_fields:
        if instance.is_root:
            # For now, root folder cannot be
            # shared. This seems a bad idea.
            instance.is_shared = False

        else:
            instance.staff_group, ___ = Group.objects.get_or_create(
                name=u'staff@folder-{0}'.format(instance.id))
            instance.members_group, ___ = Group.objects.get_or_create(
                name=u'members@folder-{0}'.format(instance.id))
            instance.guests_group, ___ = Group.objects.get_or_create(
                name=u'guests@folder-{0}'.format(instance.id))


pre_save.connect(folder_pre_save, sender=Folder)


# ————————————————————————————————————————————————————————— external properties
#                                            Defined here to avoid import loops


def User_root_folder_property_get(self):

    return Folder.get_root_for(self)


def User_top_folders_property_get(self):

    return qs_order_by_lower_name(self.folders.filter(parent=self.root_folder))


def User_folders_tree_property_get(self):
    """ Returns the full (but preference-via
        depth-limited) user folders tree as a list. """

    return self.get_folders_tree()


def User_private_folders_tree_property_get(self):
    """ Returns the full (but preference-via depth-limited)
    user non-shared folders tree as a list. """

    return self.get_folders_tree(shared=False)


def User_shared_folders_tree_property_get(self):
    """ Returns the full (but preference-via depth-limited)
        user shared folders tree as a list. """

    return self.get_folders_tree(shared=True)


def User_get_folders_tree_method(self, for_parent=False, shared=None):
    """ Return the user folder tree, excluding his/her root folder. """

    root_folder = self.root_folder

    filters = {
        'level__lte': root_folder.max_depth - (1 if for_parent else 0)
    }

    if shared is not None:
        filters['is_shared'] = shared

    # We have to filter *after* get_descendants(), not at the same time,
    # to be sure to get shared sub-folders that are locate in non-shared
    # folders.
    return root_folder.get_descendants().filter(**filters)


User.root_folder  = property(User_root_folder_property_get)
User.top_folders  = property(User_top_folders_property_get)
User.folders_tree = property(User_folders_tree_property_get)
User.shared_folders_tree  = property(User_shared_folders_tree_property_get)
User.private_folders_tree = property(User_private_folders_tree_property_get)
User.get_folders_tree = User_get_folders_tree_method
