from __future__ import absolute_import, unicode_literals

from django.conf import settings
from django.contrib import messages
from django.core.exceptions import ValidationError
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.template import RequestContext
from django.urls import reverse, reverse_lazy
from django.utils.encoding import force_text
from django.utils.http import urlencode
from django.utils.translation import ugettext_lazy as _, ungettext

from mayan.apps.acls.models import AccessControlList
from mayan.apps.common.generics import (
    FormView, MultipleObjectFormActionView, SingleObjectCreateView,
    SingleObjectDeleteView, SingleObjectEditView, SingleObjectListView
)
from mayan.apps.documents.models import Document, DocumentType
from mayan.apps.documents.permissions import (
    permission_document_type_edit
)

from .api import save_metadata_list
from .forms import (
    DocumentMetadataAddForm, DocumentMetadataFormSet,
    DocumentMetadataRemoveFormSet,
    DocumentTypeMetadataTypeRelationshipFormSet, MetadataTypeForm
)
from .icons import (
    icon_document_metadata_add, icon_document_metadata_edit,
    icon_document_metadata_remove, icon_metadata
)
from .links import (
    link_metadata_add, link_metadata_multiple_add,
    link_setup_metadata_type_create
)
from .models import DocumentMetadata, MetadataType
from .permissions import (
    permission_document_metadata_add, permission_document_metadata_edit,
    permission_document_metadata_remove, permission_document_metadata_view,
    permission_metadata_type_create, permission_metadata_type_delete,
    permission_metadata_type_edit, permission_metadata_type_view
)


class DocumentMetadataAddView(MultipleObjectFormActionView):
    form_class = DocumentMetadataAddForm
    model = Document
    object_permission = permission_document_metadata_add
    success_message = _('Metadata add request performed on %(count)d document')
    success_message_plural = _(
        'Metadata add request performed on %(count)d documents'
    )

    def dispatch(self, request, *args, **kwargs):
        result = super(
            DocumentMetadataAddView, self
        ).dispatch(request, *args, **kwargs)

        queryset = self.object_list

        for document in queryset:
            document.add_as_recent_document_for_user(request.user)

        if len(set([document.document_type.pk for document in queryset])) > 1:
            messages.error(
                message=_(
                    'Selected documents must be of the same type.'
                ), request=request
            )
            return HttpResponseRedirect(redirect_to=self.previous_url)

        return result

    def form_valid(self, form):
        result = super(DocumentMetadataAddView, self).form_valid(form=form)

        queryset = self.object_list

        if self.action_count == 1:
            return HttpResponseRedirect(
                redirect_to=reverse(
                    viewname='metadata:metadata_edit', kwargs={
                        'pk': queryset.first().pk
                    }
                )
            )
        elif self.action_count > 1:
            return HttpResponseRedirect(
                redirect_to='%s?%s' % (
                    reverse(viewname='metadata:metadata_multiple_edit'),
                    urlencode(
                        {
                            'id_list': ','.join(
                                map(
                                    force_text,
                                    queryset.values_list('pk', flat=True)
                                )
                            )
                        }
                    )
                )
            )

        return result

    def get_extra_context(self):
        queryset = self.object_list

        result = {
            'submit_icon_class': icon_document_metadata_add,
            'submit_label': _('Add'),
            'title': ungettext(
                singular='Add metadata types to document',
                plural='Add metadata types to documents',
                number=queryset.count()
            )
        }

        if queryset.count() == 1:
            result.update(
                {
                    'object': queryset.first(),
                    'title': _(
                        'Add metadata types to document: %s'
                    ) % queryset.first()
                }
            )

        return result

    def get_form_extra_kwargs(self):
        queryset = self.object_list

        result = {}

        if queryset.count():
            result.update(
                {
                    'document_type': queryset.first().document_type,
                }
            )

        if queryset.count() == 1:
            result.update(
                {
                    'queryset': MetadataType.objects.get_for_document_type(
                        document_type=queryset.first().document_type
                    ).exclude(
                        pk__in=MetadataType.objects.get_for_document(
                            document=queryset.first()
                        )
                    )
                }
            )

        return result

    def object_action(self, form, instance):
        for metadata_type in form.cleaned_data['metadata_type']:
            try:
                created = False
                try:
                    DocumentMetadata.objects.get(
                        document=instance,
                        metadata_type=metadata_type,
                    )
                except DocumentMetadata.DoesNotExist:
                    document_metadata = DocumentMetadata(
                        document=instance,
                        metadata_type=metadata_type,
                    )
                    document_metadata.save(_user=self.request.user)
                    created = True
            except Exception as exception:
                messages.error(
                    message=_(
                        'Error adding metadata type '
                        '"%(metadata_type)s" to document: '
                        '%(document)s; %(exception)s'
                    ) % {
                        'metadata_type': metadata_type,
                        'document': instance,
                        'exception': ', '.join(
                            getattr(exception, 'messages', exception)
                        )
                    }, request=self.request
                )
            else:
                if created:
                    messages.success(
                        message=_(
                            'Metadata type: %(metadata_type)s '
                            'successfully added to document %(document)s.'
                        ) % {
                            'metadata_type': metadata_type,
                            'document': instance
                        }, request=self.request
                    )
                else:
                    messages.warning(
                        message=_(
                            'Metadata type: %(metadata_type)s already '
                            'present in document %(document)s.'
                        ) % {
                            'metadata_type': metadata_type,
                            'document': instance
                        }, request=self.request
                    )


class DocumentMetadataEditView(MultipleObjectFormActionView):
    form_class = DocumentMetadataFormSet
    model = Document
    object_permission = permission_document_metadata_edit
    success_message = _(
        'Metadata edit request performed on %(count)d document'
    )
    success_message_plural = _(
        'Metadata edit request performed on %(count)d documents'
    )

    def dispatch(self, request, *args, **kwargs):
        result = super(
            DocumentMetadataEditView, self
        ).dispatch(request, *args, **kwargs)

        queryset = self.object_list

        for document in queryset:
            document.add_as_recent_document_for_user(request.user)

        if len(set([document.document_type.pk for document in queryset])) > 1:
            messages.error(
                message=_(
                    'Selected documents must be of the same type.'
                ), request=request
            )
            return HttpResponseRedirect(redirect_to=self.previous_url)

        return result

    def form_valid(self, form):
        result = super(DocumentMetadataEditView, self).form_valid(form=form)

        queryset = self.object_list

        if self.action_count == 1:
            return HttpResponseRedirect(
                redirect_to=reverse(
                    viewname='metadata:metadata_edit', kwargs={
                        'pk': queryset.first().pk
                    }
                )
            )
        elif self.action_count > 1:
            return HttpResponseRedirect(
                redirect_to='%s?%s' % (
                    reverse(viewname='metadata:metadata_multiple_edit'),
                    urlencode(
                        {
                            'id_list': ','.join(
                                map(
                                    force_text, queryset.values_list(
                                        'pk', flat=True
                                    )
                                )
                            )
                        }
                    )
                )
            )

        return result

    def get_extra_context(self):
        queryset = self.object_list

        id_list = ','.join(
            map(
                force_text,
                queryset.values_list('pk', flat=True)
            )
        )

        if queryset.count() == 1:
            no_results_main_link = link_metadata_add.resolve(
                context=RequestContext(
                    request=self.request, dict_={'object': queryset.first()}
                )
            )
        else:
            no_results_main_link = link_metadata_multiple_add.resolve(
                context=RequestContext(request=self.request)
            )
            no_results_main_link.url = '{}?id_list={}'.format(
                no_results_main_link.url, id_list
            )

        result = {
            'form_display_mode_table': True,
            'no_results_icon': icon_metadata,
            'no_results_main_link': no_results_main_link,
            'no_results_text': _(
                'Add metadata types available for this document\'s type '
                'and assign them corresponding values.'
            ),
            'no_results_title': _('There is no metadata to edit'),
            'submit_icon_class': icon_document_metadata_edit,
            'submit_label': _('Edit'),
            'title': ungettext(
                'Edit document metadata',
                'Edit documents metadata',
                queryset.count()
            )
        }

        if queryset.count() == 1:
            result.update(
                {
                    'object': queryset.first(),
                    'title': _(
                        'Edit metadata for document: %s'
                    ) % queryset.first()
                }
            )

        return result

    def get_initial(self):
        queryset = self.object_list

        metadata_dict = {}
        initial = []

        for document in queryset:
            document.add_as_recent_document_for_user(self.request.user)

            for document_metadata in document.metadata.all():
                metadata_dict.setdefault(
                    document_metadata.metadata_type, set()
                )

                if document_metadata.value:
                    metadata_dict[
                        document_metadata.metadata_type
                    ].add(document_metadata.value)

        for key, value in metadata_dict.items():
            initial.append({
                'document_type': document.document_type,
                'metadata_type': key,
                'value': ', '.join(value) if value else '',
            })

        return initial

    def object_action(self, form, instance):
        errors = []
        for form in form.forms:
            if form.cleaned_data['update']:
                try:
                    save_metadata_list(
                        metadata_list=[form.cleaned_data], document=instance,
                        _user=self.request.user
                    )
                except Exception as exception:
                    errors.append(exception)

        for error in errors:
            if settings.DEBUG:
                raise
            else:
                if isinstance(error, ValidationError):
                    exception_message = ', '.join(error.messages)
                else:
                    exception_message = force_text(error)

                messages.error(
                    message=_(
                        'Error editing metadata for document: '
                        '%(document)s; %(exception)s.'
                    ) % {
                        'document': instance,
                        'exception': exception_message
                    }, request=self.request
                )
        else:
            messages.success(
                message=_(
                    'Metadata for document %s edited successfully.'
                ) % instance, request=self.request
            )


class DocumentMetadataListView(SingleObjectListView):
    def dispatch(self, request, *args, **kwargs):
        AccessControlList.objects.check_access(
            obj=self.get_document(),
            permissions=(permission_document_metadata_view,),
            user=self.request.user
        )

        return super(DocumentMetadataListView, self).dispatch(
            request, *args, **kwargs
        )

    def get_document(self):
        return get_object_or_404(klass=Document, pk=self.kwargs['pk'])

    def get_extra_context(self):
        document = self.get_document()
        return {
            'hide_link': True,
            'hide_object': True,
            'object': document,
            'no_results_icon': icon_metadata,
            'no_results_main_link': link_metadata_add.resolve(
                context=RequestContext(
                    request=self.request, dict_={'object': document}
                )
            ),
            'no_results_text': _(
                'Add metadata types this document\'s type '
                'to be able to add them to individual documents. '
                'Once added to individual document, you can then edit their '
                'values.'
            ),
            'no_results_title': _('This document doesn\'t have any metadata'),
            'title': _('Metadata for document: %s') % document,
        }

    def get_source_queryset(self):
        return self.get_document().metadata.all()


class DocumentMetadataRemoveView(MultipleObjectFormActionView):
    form_class = DocumentMetadataRemoveFormSet
    model = Document
    object_permission = permission_document_metadata_remove
    success_message = _(
        'Metadata remove request performed on %(count)d document'
    )
    success_message_plural = _(
        'Metadata remove request performed on %(count)d documents'
    )

    def dispatch(self, request, *args, **kwargs):
        result = super(
            DocumentMetadataRemoveView, self
        ).dispatch(request, *args, **kwargs)

        queryset = self.object_list

        for document in queryset:
            document.add_as_recent_document_for_user(request.user)

        if len(set([document.document_type.pk for document in queryset])) > 1:
            messages.error(
                message=_(
                    'Selected documents must be of the same type.'
                ), request=request
            )
            return HttpResponseRedirect(redirect_to=self.previous_url)

        return result

    def form_valid(self, form):
        result = super(DocumentMetadataRemoveView, self).form_valid(form=form)
        queryset = self.object_list

        if self.action_count == 1:
            return HttpResponseRedirect(
                redirect_to=reverse(
                    viewname='metadata:metadata_edit', kwargs={
                        'pk': queryset.first().pk
                    }
                )
            )
        elif self.action_count > 1:
            return HttpResponseRedirect(
                redirect_to='%s?%s' % (
                    reverse(viewname='metadata:metadata_multiple_edit'),
                    urlencode(
                        {
                            'id_list': ','.join(
                                map(
                                    force_text,
                                    queryset.values_list('pk', flat=True)
                                )
                            )
                        }
                    )
                )
            )

        return result

    def get_extra_context(self):
        queryset = self.object_list

        result = {
            'form_display_mode_table': True,
            'submit_icon_class': icon_document_metadata_remove,
            'submit_label': _('Remove'),
            'title': ungettext(
                singular='Remove metadata types from the document',
                plural='Remove metadata types from the documents',
                number=queryset.count()
            )
        }

        if queryset.count() == 1:
            result.update(
                {
                    'object': queryset.first(),
                    'title': _(
                        'Remove metadata types from the document: %s'
                    ) % queryset.first()
                }
            )

        return result

    def get_initial(self):
        queryset = self.object_list

        metadata = {}
        for document in queryset:
            document.add_as_recent_document_for_user(self.request.user)

            for document_metadata in document.metadata.all():
                # Metadata value cannot be None here, fallback to an empty
                # string
                value = document_metadata.value or ''
                if document_metadata.metadata_type in metadata:
                    if value not in metadata[document_metadata.metadata_type]:
                        metadata[document_metadata.metadata_type].append(value)
                else:
                    metadata[document_metadata.metadata_type] = [value] if value else ''

        initial = []
        for key, value in metadata.items():
            initial.append(
                {
                    'document_type': queryset.first().document_type,
                    'metadata_type': key,
                    'value': ', '.join(value)
                }
            )
        return initial

    def object_action(self, form, instance):
        for form in form.forms:
            if form.cleaned_data['update']:
                metadata_type = get_object_or_404(
                    klass=MetadataType, pk=form.cleaned_data['id']
                )
                try:
                    document_metadata = DocumentMetadata.objects.get(
                        document=instance, metadata_type=metadata_type
                    )
                    document_metadata.delete(_user=self.request.user)
                    messages.success(
                        message=_(
                            'Successfully remove metadata type "%(metadata_type)s" from document: %(document)s.'
                        ) % {
                            'metadata_type': metadata_type,
                            'document': instance
                        }, request=self.request
                    )
                except ValidationError as exception:
                    messages.error(
                        message=_(
                            'Error removing metadata type "%(metadata_type)s" from document: %(document)s; %(exception)s'
                        ) % {
                            'metadata_type': metadata_type,
                            'document': instance,
                            'exception': ', '.join(exception.messages)
                        }, request=self.request
                    )


# Setup views
class MetadataTypeCreateView(SingleObjectCreateView):
    extra_context = {'title': _('Create metadata type')}
    form_class = MetadataTypeForm
    model = MetadataType
    post_action_redirect = reverse_lazy(
        viewname='metadata:setup_metadata_type_list'
    )
    view_permission = permission_metadata_type_create

    def get_save_extra_data(self):
        return {
            '_user': self.request.user,
        }


class MetadataTypeDeleteView(SingleObjectDeleteView):
    model = MetadataType
    object_permission = permission_metadata_type_delete
    post_action_redirect = reverse_lazy(
        viewname='metadata:setup_metadata_type_list'
    )

    def get_extra_context(self):
        return {
            'delete_view': True,
            'object': self.get_object(),
            'title': _('Delete the metadata type: %s?') % self.get_object(),
        }


class MetadataTypeEditView(SingleObjectEditView):
    form_class = MetadataTypeForm
    model = MetadataType
    object_permission = permission_metadata_type_edit
    post_action_redirect = reverse_lazy(
        viewname='metadata:setup_metadata_type_list'
    )

    def get_extra_context(self):
        return {
            'object': self.get_object(),
            'title': _('Edit metadata type: %s') % self.get_object(),
        }

    def get_save_extra_data(self):
        return {
            '_user': self.request.user,
        }


class MetadataTypeListView(SingleObjectListView):
    object_permission = permission_metadata_type_view

    def get_extra_context(self):
        return {
            'hide_link': True,
            'hide_object': True,
            'no_results_icon': icon_metadata,
            'no_results_main_link': link_setup_metadata_type_create.resolve(
                context=RequestContext(request=self.request)
            ),
            'no_results_text': _(
                'Metadata types are users defined properties that can be '
                'assigned values. Once created they must be associated to '
                'document types, either as optional or required, for each. '
                'Setting a metadata type as required for a document type '
                'will block the upload of documents of that type until a '
                'metadata value is provided.'
            ),
            'no_results_title': _('There are no metadata types'),
            'title': _('Metadata types'),
        }

    def get_source_queryset(self):
        return MetadataType.objects.all()


class SetupDocumentTypeMetadataTypes(FormView):
    form_class = DocumentTypeMetadataTypeRelationshipFormSet
    main_model = 'document_type'
    model = DocumentType
    submodel = MetadataType

    def form_valid(self, form):
        try:
            for instance in form:
                instance.save()
        except Exception as exception:
            messages.error(
                message=_(
                    'Error updating relationship; %s'
                ) % exception, request=self.request
            )
        else:
            messages.success(
                message=_('Relationships updated successfully'),
                request=self.request
            )

        return super(
            SetupDocumentTypeMetadataTypes, self
        ).form_valid(form=form)

    def get_extra_context(self):
        return {
            'form_display_mode_table': True,
            'no_results_icon': icon_metadata,
            'no_results_main_link': link_setup_metadata_type_create.resolve(
                context=RequestContext(request=self.request)
            ),
            'no_results_text': _(
                'Create metadata types to be able to associate them '
                'to this document type.'
            ),
            'no_results_title': _('There are no metadata types available'),
            'object': self.get_object(),
            'title': _(
                'Metadata types for document type: %s'
            ) % self.get_object()
        }

    def get_form_extra_kwargs(self):
        return {
            '_user': self.request.user,
        }

    def get_initial(self):
        obj = self.get_object()
        initial = []

        for element in self.get_queryset():
            initial.append({
                'document_type': obj,
                'main_model': self.main_model,
                'metadata_type': element,
            })
        return initial

    def get_object(self):
        obj = get_object_or_404(klass=self.model, pk=self.kwargs['pk'])

        AccessControlList.objects.check_access(
            obj=obj, permissions=(permission_metadata_type_edit,),
            user=self.request.user
        )
        return obj

    def get_post_action_redirect(self):
        return reverse(viewname='documents:document_type_list')

    def get_queryset(self):
        queryset = self.submodel.objects.all()
        return AccessControlList.objects.restrict_queryset(
            permission=permission_document_type_edit,
            user=self.request.user, queryset=queryset
        )


class SetupMetadataTypesDocumentTypes(SetupDocumentTypeMetadataTypes):
    main_model = 'metadata_type'
    model = MetadataType
    submodel = DocumentType

    def get_extra_context(self):
        return {
            'form_display_mode_table': True,
            'object': self.get_object(),
            'title': _(
                'Document types for metadata type: %s'
            ) % self.get_object()
        }

    def get_initial(self):
        obj = self.get_object()
        initial = []

        for element in self.get_queryset():
            initial.append({
                'document_type': element,
                'main_model': self.main_model,
                'metadata_type': obj,
            })
        return initial

    def get_post_action_redirect(self):
        return reverse(viewname='metadata:setup_metadata_type_list')
