from rest_framework import serializers

from django.core.exceptions import PermissionDenied, ValidationError
from django.http import Http404
from django.utils import six
from django.utils.translation import ugettext as _, ugettext_lazy, ungettext

from misago.acl import add_acl
from misago.categories import THREADS_ROOT_NAME
from misago.conf import settings
from misago.threads.models import Thread
from misago.threads.permissions import (
    allow_delete_event, allow_delete_post, allow_delete_thread,
    allow_merge_post, allow_merge_thread,
    allow_move_post, allow_split_post,
    can_reply_thread, can_see_thread,
    can_start_thread, exclude_invisible_posts)
from misago.threads.pollmergehandler import PollMergeHandler
from misago.threads.threadtypes import trees_map
from misago.threads.utils import get_thread_id_from_url
from misago.threads.validators import validate_category, validate_title


POSTS_LIMIT = settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL
THREADS_LIMIT = settings.MISAGO_THREADS_PER_PAGE + settings.MISAGO_THREADS_TAIL


__all__ = [
    'DeletePostsSerializer',
    'DeleteThreadsSerializer',
    'MergePostsSerializer',
    'MergeThreadSerializer',
    'MergeThreadsSerializer',
    'MovePostsSerializer',
    'NewThreadSerializer',
    'SplitPostsSerializer',
]


class DeletePostsSerializer(serializers.Serializer):
    error_empty_or_required = ugettext_lazy("You have to specify at least one post to delete.")

    posts = serializers.ListField(
        allow_empty=False,
        child=serializers.IntegerField(
            error_messages={
                'invalid': ugettext_lazy("One or more post ids received were invalid."),
            },
        ),
        error_messages={
            'required': error_empty_or_required,
            'null': error_empty_or_required,
            'empty': error_empty_or_required,
        },
    )

    def validate_posts(self, data):
        if len(data) > POSTS_LIMIT:
            message = ungettext(
                "No more than %(limit)s post can be deleted at single time.",
                "No more than %(limit)s posts can be deleted at single time.",
                POSTS_LIMIT,
            )
            raise ValidationError(message % {'limit': POSTS_LIMIT})

        user = self.context['user']
        thread = self.context['thread']

        posts_queryset = exclude_invisible_posts(user, thread.category, thread.post_set)
        posts_queryset = posts_queryset.filter(id__in=data).order_by('id')

        posts = []
        for post in posts_queryset:
            post.category = thread.category
            post.thread = thread

            if post.is_event:
                allow_delete_event(user, post)
            else:
                allow_delete_post(user, post)

            posts.append(post)

        if len(posts) != len(data):
            raise PermissionDenied(_("One or more posts to delete could not be found."))

        return posts


class MergePostsSerializer(serializers.Serializer):
    error_empty_or_required = ugettext_lazy("You have to select at least two posts to merge.")

    posts = serializers.ListField(
        child=serializers.IntegerField(
            error_messages={
                'invalid': ugettext_lazy("One or more post ids received were invalid."),
            },
        ),
        error_messages={
            'null': error_empty_or_required,
            'required': error_empty_or_required,
        },
    )

    def validate_posts(self, data):
        data = list(set(data))

        if len(data) < 2:
            raise serializers.ValidationError(self.error_empty_or_required)
        if len(data) > POSTS_LIMIT:
            message = ungettext(
                "No more than %(limit)s post can be merged at single time.",
                "No more than %(limit)s posts can be merged at single time.",
                POSTS_LIMIT,
            )
            raise serializers.ValidationError(message % {'limit': POSTS_LIMIT})

        user = self.context['user']
        thread = self.context['thread']

        posts_queryset = exclude_invisible_posts(user, thread.category, thread.post_set)
        posts_queryset = posts_queryset.filter(id__in=data).order_by('id')

        posts = []
        for post in posts_queryset:
            post.category = thread.category
            post.thread = thread

            try:
                allow_merge_post(user, post)
            except PermissionDenied as e:
                raise serializers.ValidationError(e)

            if not posts:
                posts.append(post)
            else:
                authorship_error = _("Posts made by different users can't be merged.")
                if posts[0].poster_id:
                    if post.poster_id != posts[0].poster_id:
                        raise serializers.ValidationError(authorship_error)
                else:
                    if post.poster_id or post.poster_name != posts[0].poster_name:
                        raise serializers.ValidationError(authorship_error)

                if posts[0].pk != thread.first_post_id:
                    if (posts[0].is_hidden != post.is_hidden or
                            posts[0].is_unapproved != post.is_unapproved):
                        raise serializers.ValidationError(
                            _("Posts with different visibility can't be merged.")
                        )

                posts.append(post)

        if len(posts) != len(data):
            raise serializers.ValidationError(_("One or more posts to merge could not be found."))

        return posts


class MovePostsSerializer(serializers.Serializer):
    error_empty_or_required = ugettext_lazy("You have to specify at least one post to move.")

    new_thread = serializers.CharField(
        error_messages={
            'required': ugettext_lazy("Enter link to new thread."),
        },
    )
    posts = serializers.ListField(
        allow_empty=False,
        child=serializers.IntegerField(
            error_messages={
                'invalid': ugettext_lazy("One or more post ids received were invalid."),
            },
        ),
        error_messages={
            'empty': error_empty_or_required,
            'null': error_empty_or_required,
            'required': error_empty_or_required,
        },
    )

    def validate_new_thread(self, data):
        request = self.context['request']
        thread = self.context['thread']
        viewmodel = self.context['viewmodel']

        new_thread_id = get_thread_id_from_url(request, data)
        if not new_thread_id:
            raise serializers.ValidationError(_("This is not a valid thread link."))
        if new_thread_id == thread.pk:
            raise serializers.ValidationError(_("Thread to move posts to is same as current one."))

        try:
            new_thread = viewmodel(request, new_thread_id).unwrap()
        except Http404:
            raise serializers.ValidationError(
                _(
                    "The thread you have entered link to doesn't "
                    "exist or you don't have permission to see it."
                )
            )

        if not new_thread.acl['can_reply']:
            raise serializers.ValidationError(_("You can't move posts to threads you can't reply."))

        return new_thread

    def validate_posts(self, data):
        data = list(set(data))
        if len(data) > POSTS_LIMIT:
            message = ungettext(
                "No more than %(limit)s post can be moved at single time.",
                "No more than %(limit)s posts can be moved at single time.",
                POSTS_LIMIT,
            )
            raise serializers.ValidationError(message % {'limit': POSTS_LIMIT})

        request = self.context['request']
        thread = self.context['thread']

        posts_queryset = exclude_invisible_posts(request.user, thread.category, thread.post_set)
        posts_queryset = posts_queryset.filter(id__in=data).order_by('id')

        posts = []
        for post in posts_queryset:
            post.category = thread.category
            post.thread = thread

            try:
                allow_move_post(request.user, post)
                posts.append(post)
            except PermissionDenied as e:
                raise serializers.ValidationError(e)

        if len(posts) != len(data):
            raise serializers.ValidationError(_("One or more posts to move could not be found."))

        return posts


class NewThreadSerializer(serializers.Serializer):
    title = serializers.CharField()
    category = serializers.IntegerField()
    weight = serializers.IntegerField(
        required=False,
        allow_null=True,
        max_value=Thread.WEIGHT_GLOBAL,
        min_value=Thread.WEIGHT_DEFAULT,
    )
    is_hidden = serializers.NullBooleanField(required=False)
    is_closed = serializers.NullBooleanField(required=False)

    def validate_title(self, title):
        return validate_title(title)

    def validate_category(self, category_id):
        self.category = validate_category(self.context['user'], category_id)
        if not can_start_thread(self.context['user'], self.category):
            raise ValidationError(_("You can't create new threads in selected category."))
        return self.category

    def validate_weight(self, weight):
        try:
            add_acl(self.context['user'], self.category)
        except AttributeError:
            return weight  # don't validate weight further if category failed

        if weight > self.category.acl.get('can_pin_threads', 0):
            if weight == 2:
                raise ValidationError(
                    _("You don't have permission to pin threads globally in this category.")
                )
            else:
                raise ValidationError(
                    _("You don't have permission to pin threads in this category.")
                )
        return weight

    def validate_is_hidden(self, is_hidden):
        try:
            add_acl(self.context['user'], self.category)
        except AttributeError:
            return is_hidden  # don't validate hidden further if category failed

        if is_hidden and not self.category.acl.get('can_hide_threads'):
            raise ValidationError(_("You don't have permission to hide threads in this category."))
        return is_hidden

    def validate_is_closed(self, is_closed):
        try:
            add_acl(self.context['user'], self.category)
        except AttributeError:
            return is_closed  # don't validate closed further if category failed

        if is_closed and not self.category.acl.get('can_close_threads'):
            raise ValidationError(
                _("You don't have permission to close threads in this category.")
            )
        return is_closed


class SplitPostsSerializer(NewThreadSerializer):
    error_empty_or_required = ugettext_lazy("You have to specify at least one post to split.")

    posts = serializers.ListField(
        allow_empty=False,
        child=serializers.IntegerField(
            error_messages={
                'invalid': ugettext_lazy("One or more post ids received were invalid."),
            },
        ),
        error_messages={
            'empty': error_empty_or_required,
            'null': error_empty_or_required,
            'required': error_empty_or_required,
        },
    )

    def validate_posts(self, data):
        if len(data) > POSTS_LIMIT:
            message = ungettext(
                "No more than %(limit)s post can be split at single time.",
                "No more than %(limit)s posts can be split at single time.",
                POSTS_LIMIT,
            )
            raise ValidationError(message % {'limit': POSTS_LIMIT})

        thread = self.context['thread']
        user = self.context['user']

        posts_queryset = exclude_invisible_posts(user, thread.category, thread.post_set)
        posts_queryset = posts_queryset.filter(id__in=data).order_by('id')

        posts = []
        for post in posts_queryset:
            post.category = thread.category
            post.thread = thread

            try:
                allow_split_post(user, post)
            except PermissionDenied as e:
                raise ValidationError(e)

            posts.append(post)

        if len(posts) != len(data):
            raise ValidationError(_("One or more posts to split could not be found."))

        return posts


class DeleteThreadsSerializer(serializers.Serializer):
    error_empty_or_required = ugettext_lazy("You have to specify at least one thread to delete.")

    threads = serializers.ListField(
        allow_empty=False,
        child=serializers.IntegerField(
            error_messages={
                'invalid': ugettext_lazy("One or more thread ids received were invalid."),
            },
        ),
        error_messages={
            'required': error_empty_or_required,
            'null': error_empty_or_required,
            'empty': error_empty_or_required,
        },
    )

    def validate_threads(self, data):
        if len(data) > THREADS_LIMIT:
            message = ungettext(
                "No more than %(limit)s thread can be deleted at single time.",
                "No more than %(limit)s threads can be deleted at single time.",
                THREADS_LIMIT,
            )
            raise ValidationError(message % {'limit': THREADS_LIMIT})

        request = self.context['request']
        viewmodel = self.context['viewmodel']

        threads = []
        errors = []

        for thread_id in data:
            try:
                thread = viewmodel(request, thread_id).unwrap()
                allow_delete_thread(request.user, thread)
                threads.append(thread)
            except PermissionDenied as e:
                errors.append({
                    'thread': {
                        'id': thread.id,
                        'title': thread.title
                    },
                    'error': six.text_type(e)
                })
            except Http404 as e:
                pass # skip invisible threads

        if errors:
            raise serializers.ValidationError({'details': errors})

        if len(threads) != len(data):
            raise ValidationError(_("One or more threads to delete could not be found."))

        return threads


class MergeThreadSerializer(serializers.Serializer):
    other_thread = serializers.CharField(
        error_messages={
            'required': ugettext_lazy("Enter link to new thread."),
        },
    )
    poll = serializers.IntegerField(
        required=False,
        error_messages={
            'invalid': ugettext_lazy("Invalid choice."),
        },
    )

    def validate_other_thread(self, data):
        request = self.context['request']
        thread = self.context['thread']
        viewmodel = self.context['viewmodel']

        other_thread_id = get_thread_id_from_url(request, data)
        if not other_thread_id:
            raise ValidationError(_("This is not a valid thread link."))
        if other_thread_id == thread.pk:
            raise ValidationError(_("You can't merge thread with itself."))

        try:
            other_thread = viewmodel(request, other_thread_id).unwrap()
            allow_merge_thread(request.user, other_thread, otherthread=True)
        except PermissionDenied as e:
            raise serializers.ValidationError(e)
        except Http404:
            raise ValidationError(
                _(
                    "The thread you have entered link to doesn't "
                    "exist or you don't have permission to see it."
                )
            )

        if not can_reply_thread(request.user, other_thread):
            raise ValidationError(_("You can't merge this thread into thread you can't reply."))

        return other_thread

    def validate(self, data):
        thread = self.context['thread']
        other_thread = data['other_thread']

        polls_handler = PollMergeHandler([thread, other_thread])

        if len(polls_handler.polls) == 1:
            data['poll'] = polls_handler.polls[0]
        elif polls_handler.is_merge_conflict():
            if 'poll' in data:
                polls_handler.set_resolution(data['poll'])
                if polls_handler.is_valid():
                    data['poll'] = polls_handler.get_resolution()
                else:
                    raise serializers.ValidationError({'poll': _("Invalid choice.")})
            else:
                data['polls'] = polls_handler.get_available_resolutions()

        self.polls_handler = polls_handler

        return data


class MergeThreadsSerializer(NewThreadSerializer):
    error_empty_or_required = ugettext_lazy("You have to select at least two threads to merge.")

    threads = serializers.ListField(
        allow_empty=False,
        min_length=2,
        child=serializers.IntegerField(
            error_messages={
                'invalid': ugettext_lazy("One or more thread ids received were invalid."),
            },
        ),
        error_messages={
            'empty': error_empty_or_required,
            'null': error_empty_or_required,
            'required': error_empty_or_required,
            'min_length': error_empty_or_required,
        },
    )

    def validate_threads(self, data):
        if len(data) > THREADS_LIMIT:
            message = ungettext(
                "No more than %(limit)s thread can be merged at single time.",
                "No more than %(limit)s threads can be merged at single time.",
                POSTS_LIMIT,
            )
            raise ValidationError(message % {'limit': THREADS_LIMIT})

        threads_tree_id = trees_map.get_tree_id_for_root(THREADS_ROOT_NAME)

        threads_queryset = Thread.objects.filter(
            id__in=data,
            category__tree_id=threads_tree_id,
        ).select_related('category').order_by('-id')

        user = self.context['user']

        threads = []
        for thread in threads_queryset:
            add_acl(user, thread)
            if can_see_thread(user, thread):
                threads.append(thread)

        if len(threads) != len(data):
            raise ValidationError(_("One or more threads to merge could not be found."))

        return threads
