"""
Course API Forms
"""
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.forms import Form, CharField, ChoiceField, IntegerField
from django.http import Http404
from rest_framework.exceptions import PermissionDenied

from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import UsageKey
from openedx.core.djangoapps.util.forms import ExtendedNullBooleanField, MultiValueField
from xmodule.modulestore.django import modulestore

from . import permissions


class BlockListGetForm(Form):
    """
    A form to validate query parameters in the block list retrieval endpoint
    """
    all_blocks = ExtendedNullBooleanField(required=False)
    block_counts = MultiValueField(required=False)
    depth = CharField(required=False)
    nav_depth = IntegerField(required=False, min_value=0)
    requested_fields = MultiValueField(required=False)
    return_type = ChoiceField(
        required=False,
        choices=[(choice, choice) for choice in ['dict', 'list']],
    )
    student_view_data = MultiValueField(required=False)
    usage_key = CharField(required=True)
    username = CharField(required=False)
    block_types_filter = MultiValueField(required=False)

    def clean_depth(self):
        """
        Get the appropriate depth.  No provided value will be treated as a
        depth of 0, while a value of "all" will be treated as unlimited depth.
        """
        value = self.cleaned_data['depth']
        if not value:
            return 0
        elif value == "all":
            return None
        try:
            return int(value)
        except ValueError:
            raise ValidationError("'{}' is not a valid depth value.".format(value))

    def clean_requested_fields(self):
        """
        Return a set of `requested_fields`, merged with defaults of `type`
        and `display_name`
        """
        requested_fields = self.cleaned_data['requested_fields']

        # add default requested_fields
        return (requested_fields or set()) | {'type', 'display_name'}

    def clean_return_type(self):
        """
        Return valid 'return_type' or default value of 'dict'
        """
        return self.cleaned_data['return_type'] or 'dict'

    def clean_usage_key(self):
        """
        Ensure a valid `usage_key` was provided.
        """
        usage_key = self.cleaned_data['usage_key']

        try:
            usage_key = UsageKey.from_string(usage_key)
        except InvalidKeyError:
            raise ValidationError("'{}' is not a valid usage key.".format(unicode(usage_key)))

        return usage_key.replace(course_key=modulestore().fill_in_run(usage_key.course_key))

    def clean(self):
        """
        Return cleaned data, including additional requested fields.
        """
        cleaned_data = super(BlockListGetForm, self).clean()

        # Add additional requested_fields that are specified as separate
        # parameters, if they were requested.
        additional_requested_fields = [
            'student_view_data',
            'block_counts',
            'nav_depth',
            'block_types_filter',
        ]
        for additional_field in additional_requested_fields:
            field_value = cleaned_data.get(additional_field)
            if field_value or field_value == 0:  # allow 0 as a requested value
                cleaned_data['requested_fields'].add(additional_field)

        usage_key = cleaned_data.get('usage_key')
        if not usage_key:
            return

        cleaned_data['user'] = self._clean_requested_user(cleaned_data, usage_key.course_key)
        return cleaned_data

    def _clean_requested_user(self, cleaned_data, course_key):
        """
        Validates and returns the requested_user, while checking permissions.
        """
        requesting_user = self.initial['requesting_user']
        requested_username = cleaned_data.get('username', None)

        if not requested_username:
            return self._verify_no_user(requesting_user, cleaned_data, course_key)
        elif requesting_user.username.lower() == requested_username.lower():
            return self._verify_requesting_user(requesting_user, course_key)
        else:
            return self._verify_other_user(requesting_user, requested_username, course_key)

    @staticmethod
    def _verify_no_user(requesting_user, cleaned_data, course_key):
        """
        Verifies form for when no username is specified, including permissions.
        """
        # Verify that access to all blocks is requested
        # (and not unintentionally requested).
        if not cleaned_data.get('all_blocks', None):
            raise ValidationError({'username': ['This field is required unless all_blocks is requested.']})

        # Verify all blocks can be accessed for the course.
        if not permissions.can_access_all_blocks(requesting_user, course_key):
            raise PermissionDenied(
                "'{requesting_username}' does not have permission to access all blocks in '{course_key}'."
                .format(requesting_username=requesting_user.username, course_key=unicode(course_key))
            )

        # return None for user
        return None

    @staticmethod
    def _verify_requesting_user(requesting_user, course_key):
        """
        Verifies whether the requesting user can access blocks in the course.
        """
        if not permissions.can_access_self_blocks(requesting_user, course_key):
            raise PermissionDenied(
                "Course blocks for '{requesting_username}' cannot be accessed."
                .format(requesting_username=requesting_user.username)
            )
        return requesting_user

    @staticmethod
    def _verify_other_user(requesting_user, requested_username, course_key):
        """
        Verifies whether the requesting user can access another user's view of
        the blocks in the course.
        """
        # Verify requesting user can access the user's blocks.
        if not permissions.can_access_others_blocks(requesting_user, course_key):
            raise PermissionDenied(
                "'{requesting_username}' does not have permission to access view for '{requested_username}'."
                .format(requesting_username=requesting_user.username, requested_username=requested_username)
            )

        # Verify user exists.
        try:
            return User.objects.get(username=requested_username)
        except User.DoesNotExist:
            raise Http404(
                "Requested user '{requested_username}' does not exist.".format(requested_username=requested_username)
            )