""" API v0 views. """

import datetime
import json
import logging

import pytz
from ccx_keys.locator import CCXLocator
from django.contrib.auth.models import User
from django.db import transaction
from django.http import Http404
from edx_rest_framework_extensions.authentication import JwtAuthentication
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey, UsageKey
from rest_framework import status
from rest_framework.generics import GenericAPIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response

from courseware import courses
from lms.djangoapps.ccx.models import CcxFieldOverride, CustomCourseForEdX
from lms.djangoapps.ccx.overrides import override_field_for_ccx
from lms.djangoapps.ccx.utils import (
    add_master_course_staff_to_ccx,
    assign_staff_role_to_ccx,
    get_course_chapters,
    is_email
)
from lms.djangoapps.instructor.enrollment import enroll_email, get_email_params
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.lib.api import authentication, permissions
from student.models import CourseEnrollment
from student.roles import CourseCcxCoachRole
from xmodule.modulestore.django import SignalHandler

from .paginators import CCXAPIPagination
from .serializers import CCXCourseSerializer

log = logging.getLogger(__name__)
TODAY = datetime.datetime.today  # for patching in tests


def get_valid_course(course_id, is_ccx=False, advanced_course_check=False):
    """
    Helper function used to validate and get a course from a course_id string.
    It works with both master and ccx course id.

    Args:
        course_id (str): A string representation of a Master or CCX Course ID.
        is_ccx (bool): Flag to perform the right validation
        advanced_course_check (bool): Flag to perform extra validations for the master course

    Returns:
        tuple: a tuple of course_object, course_key, error_code, http_status_code
    """
    if course_id is None:
        # the ccx detail view cannot call this function with a "None" value
        # so the following `error_code` should be never used, but putting it
        # to avoid a `NameError` exception in case this function will be used
        # elsewhere in the future
        error_code = 'course_id_not_provided'
        if not is_ccx:
            log.info('Master course ID not provided')
            error_code = 'master_course_id_not_provided'

        return None, None, error_code, status.HTTP_400_BAD_REQUEST

    try:
        course_key = CourseKey.from_string(course_id)
    except InvalidKeyError:
        log.info('Course ID string "%s" is not valid', course_id)
        return None, None, 'course_id_not_valid', status.HTTP_400_BAD_REQUEST

    if not is_ccx:
        try:
            course_object = courses.get_course_by_id(course_key)
        except Http404:
            log.info('Master Course with ID "%s" not found', course_id)
            return None, None, 'course_id_does_not_exist', status.HTTP_404_NOT_FOUND
        if advanced_course_check:
            if course_object.id.deprecated:
                return None, None, 'deprecated_master_course_id', status.HTTP_400_BAD_REQUEST
            if not course_object.enable_ccx:
                return None, None, 'ccx_not_enabled_for_master_course', status.HTTP_403_FORBIDDEN
        return course_object, course_key, None, None
    else:
        try:
            ccx_id = course_key.ccx
        except AttributeError:
            log.info('Course ID string "%s" is not a valid CCX ID', course_id)
            return None, None, 'course_id_not_valid_ccx_id', status.HTTP_400_BAD_REQUEST
        # get the master_course key
        master_course_key = course_key.to_course_locator()
        try:
            ccx_course = CustomCourseForEdX.objects.get(id=ccx_id, course_id=master_course_key)
            return ccx_course, course_key, None, None
        except CustomCourseForEdX.DoesNotExist:
            log.info('CCX Course with ID "%s" not found', course_id)
            return None, None, 'ccx_course_id_does_not_exist', status.HTTP_404_NOT_FOUND


def get_valid_input(request_data, ignore_missing=False):
    """
    Helper function to validate the data sent as input and to
    build field based errors.

    Args:
        request_data (OrderedDict): the request data object
        ignore_missing (bool): whether or not to ignore fields
            missing from the input data

    Returns:
        tuple: a tuple of two dictionaries for valid input and field errors
    """
    valid_input = {}
    field_errors = {}
    mandatory_fields = ('coach_email', 'display_name', 'max_students_allowed',)

    # checking first if all the fields are present and they are not null
    if not ignore_missing:
        for field in mandatory_fields:
            if field not in request_data:
                field_errors[field] = {'error_code': 'missing_field_{0}'.format(field)}
        if field_errors:
            return valid_input, field_errors

    # at this point I can assume that if the fields are present,
    # they must be validated, otherwise they can be skipped
    coach_email = request_data.get('coach_email')
    if coach_email is not None:
        if is_email(coach_email):
            valid_input['coach_email'] = coach_email
        else:
            field_errors['coach_email'] = {'error_code': 'invalid_coach_email'}
    elif 'coach_email' in request_data:
        field_errors['coach_email'] = {'error_code': 'null_field_coach_email'}

    display_name = request_data.get('display_name')
    if display_name is not None:
        if not display_name:
            field_errors['display_name'] = {'error_code': 'invalid_display_name'}
        else:
            valid_input['display_name'] = display_name
    elif 'display_name' in request_data:
        field_errors['display_name'] = {'error_code': 'null_field_display_name'}

    max_students_allowed = request_data.get('max_students_allowed')
    if max_students_allowed is not None:
        try:
            max_students_allowed = int(max_students_allowed)
            valid_input['max_students_allowed'] = max_students_allowed
        except (TypeError, ValueError):
            field_errors['max_students_allowed'] = {'error_code': 'invalid_max_students_allowed'}
    elif 'max_students_allowed' in request_data:
        field_errors['max_students_allowed'] = {'error_code': 'null_field_max_students_allowed'}

    course_modules = request_data.get('course_modules')
    if course_modules is not None:
        if isinstance(course_modules, list):
            # de-duplicate list of modules
            course_modules = list(set(course_modules))
            for course_module_id in course_modules:
                try:
                    UsageKey.from_string(course_module_id)
                except InvalidKeyError:
                    field_errors['course_modules'] = {'error_code': 'invalid_course_module_keys'}
                    break
            else:
                valid_input['course_modules'] = course_modules
        else:
            field_errors['course_modules'] = {'error_code': 'invalid_course_module_list'}
    elif 'course_modules' in request_data:
        # case if the user actually passed null as input
        valid_input['course_modules'] = None

    return valid_input, field_errors


def valid_course_modules(course_module_list, master_course_key):
    """
    Function to validate that each element in the course_module_list belongs
    to the master course structure.
    Args:
        course_module_list (list): A list of strings representing Block Usage Keys
        master_course_key (CourseKey): An object representing the master course key id

    Returns:
        bool: whether or not all the course module strings belong to the master course
    """
    course_chapters = get_course_chapters(master_course_key)
    if course_chapters is None:
        return False
    return set(course_module_list).intersection(set(course_chapters)) == set(course_module_list)


def make_user_coach(user, master_course_key):
    """
    Makes an user coach on the master course.
    This function is needed because an user cannot become a coach of the CCX if s/he is not
    coach on the master course.

    Args:
        user (User): User object
        master_course_key (CourseKey): Key locator object for the course
    """
    coach_role_on_master_course = CourseCcxCoachRole(master_course_key)
    coach_role_on_master_course.add_users(user)


class CCXListView(GenericAPIView):
    """
        **Use Case**

            * Get the list of CCX courses for a given master course.

            * Creates a new CCX course for a given master course.

        **Example Request**

            GET /api/ccx/v0/ccx/?master_course_id={master_course_id}

            POST /api/ccx/v0/ccx {

                "master_course_id": "course-v1:Organization+EX101+RUN-FALL2099",
                "display_name": "CCX example title",
                "coach_email": "john@example.com",
                "max_students_allowed": 123,
                "course_modules" : [
                    "block-v1:Organization+EX101+RUN-FALL2099+type@chapter+block@week1",
                    "block-v1:Organization+EX101+RUN-FALL2099+type@chapter+block@week4",
                    "block-v1:Organization+EX101+RUN-FALL2099+type@chapter+block@week5"
                ]

            }

        **GET Parameters**

            A GET request can include the following parameters.

            * master_course_id: A string representation of a Master Course ID. Note that this must be properly
              encoded by the client.

            * page: Optional. An integer representing the pagination instance number.

            * order_by: Optional. A string representing the field by which sort the results.

            * sort_order: Optional. A string (either "asc" or "desc") indicating the desired order.

        **POST Parameters**

            A POST request can include the following parameters.

            * master_course_id: A string representation of a Master Course ID.

            * display_name: A string representing the CCX Course title.

            * coach_email: A string representing the CCX owner email.

            * max_students_allowed: An integer representing he maximum number of students that
              can be enrolled in the CCX Course.

            * course_modules: Optional. A list of course modules id keys.

        **GET Response Values**

            If the request for information about the course is successful, an HTTP 200 "OK" response
            is returned with a collection of CCX courses for the specified master course.

            The HTTP 200 response has the following values.

            * results: a collection of CCX courses. Each CCX course contains the following values:

                * ccx_course_id: A string representation of a CCX Course ID.

                * display_name: A string representing the CCX Course title.

                * coach_email: A string representing the CCX owner email.

                * start: A string representing the start date for the CCX Course.

                * due: A string representing the due date for the CCX Course.

                * max_students_allowed: An integer representing he maximum number of students that
                  can be enrolled in the CCX Course.

                * course_modules: A list of course modules id keys.

            * count: An integer representing the total number of records that matched the request parameters.

            * next: A string representing the URL where to retrieve the next page of results. This can be `null`
              in case the response contains the complete list of results.

            * previous: A string representing the URL where to retrieve the previous page of results. This can be
              `null` in case the response contains the first page of results.

        **Example GET Response**

            {
                "count": 99,
                "next": "https://openedx-ccx-api-instance.org/api/ccx/v0/ccx/?course_id=<course_id>&page=2",
                "previous": null,
                "results": {
                    {
                        "ccx_course_id": "ccx-v1:Organization+EX101+RUN-FALL2099+ccx@1",
                        "display_name": "CCX example title",
                        "coach_email": "john@example.com",
                        "start": "2019-01-01",
                        "due": "2019-06-01",
                        "max_students_allowed": 123,
                        "course_modules" : [
                            "block-v1:Organization+EX101+RUN-FALL2099+type@chapter+block@week1",
                            "block-v1:Organization+EX101+RUN-FALL2099+type@chapter+block@week4",
                            "block-v1:Organization+EX101+RUN-FALL2099+type@chapter+block@week5"
                        ]
                    },
                    { ... }
                }
            }

        **POST Response Values**

            If the request for the creation of a CCX Course is successful, an HTTP 201 "Created" response
            is returned with the newly created CCX details.

            The HTTP 201 response has the following values.

            * ccx_course_id: A string representation of a CCX Course ID.

            * display_name: A string representing the CCX Course title.

            * coach_email: A string representing the CCX owner email.

            * start: A string representing the start date for the CCX Course.

            * due: A string representing the due date for the CCX Course.

            * max_students_allowed: An integer representing he maximum number of students that
              can be enrolled in the CCX Course.

            * course_modules: A list of course modules id keys.

        **Example POST Response**

            {
                "ccx_course_id": "ccx-v1:Organization+EX101+RUN-FALL2099+ccx@1",
                "display_name": "CCX example title",
                "coach_email": "john@example.com",
                "start": "2019-01-01",
                "due": "2019-06-01",
                "max_students_allowed": 123,
                "course_modules" : [
                    "block-v1:Organization+EX101+RUN-FALL2099+type@chapter+block@week1",
                    "block-v1:Organization+EX101+RUN-FALL2099+type@chapter+block@week4",
                    "block-v1:Organization+EX101+RUN-FALL2099+type@chapter+block@week5"
                ]
    }
    """
    authentication_classes = (
        JwtAuthentication,
        authentication.OAuth2AuthenticationAllowInactiveUser,
        authentication.SessionAuthenticationAllowInactiveUser,
    )
    permission_classes = (IsAuthenticated, permissions.IsMasterCourseStaffInstructor)
    serializer_class = CCXCourseSerializer
    pagination_class = CCXAPIPagination

    def get(self, request):
        """
        Gets a list of CCX Courses for a given Master Course.

        Additional parameters are allowed for pagination purposes.

        Args:
            request (Request): Django request object.

        Return:
            A JSON serialized representation of a list of CCX courses.
        """
        master_course_id = request.GET.get('master_course_id')
        master_course_object, master_course_key, error_code, http_status = get_valid_course(master_course_id)
        if master_course_object is None:
            return Response(
                status=http_status,
                data={
                    'error_code': error_code
                }
            )

        queryset = CustomCourseForEdX.objects.filter(course_id=master_course_key)
        order_by_input = request.query_params.get('order_by')
        sort_order_input = request.query_params.get('sort_order')
        if order_by_input in ('id', 'display_name'):
            sort_direction = ''
            if sort_order_input == 'desc':
                sort_direction = '-'
            queryset = queryset.order_by('{0}{1}'.format(sort_direction, order_by_input))
        page = self.paginate_queryset(queryset)
        serializer = self.get_serializer(page, many=True)
        response = self.get_paginated_response(serializer.data)
        return response

    def post(self, request):
        """
        Creates a new CCX course for a given Master Course.

        Args:
            request (Request): Django request object.

        Return:
            A JSON serialized representation a newly created CCX course.
        """
        master_course_id = request.data.get('master_course_id')
        master_course_object, master_course_key, error_code, http_status = get_valid_course(
            master_course_id,
            advanced_course_check=True
        )
        if master_course_object is None:
            return Response(
                status=http_status,
                data={
                    'error_code': error_code
                }
            )

        # validating the rest of the input
        valid_input, field_errors = get_valid_input(request.data)
        if field_errors:
            return Response(
                status=status.HTTP_400_BAD_REQUEST,
                data={
                    'field_errors': field_errors
                }
            )

        try:
            coach = User.objects.get(email=valid_input['coach_email'])
        except User.DoesNotExist:
            return Response(
                status=status.HTTP_404_NOT_FOUND,
                data={
                    'error_code': 'coach_user_does_not_exist'
                }
            )

        if valid_input.get('course_modules'):
            if not valid_course_modules(valid_input['course_modules'], master_course_key):
                return Response(
                    status=status.HTTP_400_BAD_REQUEST,
                    data={
                        'error_code': 'course_module_list_not_belonging_to_master_course'
                    }
                )
        # prepare the course_modules to be stored in a json stringified field
        course_modules_json = json.dumps(valid_input.get('course_modules'))

        with transaction.atomic():
            ccx_course_object = CustomCourseForEdX(
                course_id=master_course_object.id,
                coach=coach,
                display_name=valid_input['display_name'],
                structure_json=course_modules_json
            )
            ccx_course_object.save()

            # Make sure start/due are overridden for entire course
            start = TODAY().replace(tzinfo=pytz.UTC)
            override_field_for_ccx(ccx_course_object, master_course_object, 'start', start)
            override_field_for_ccx(ccx_course_object, master_course_object, 'due', None)

            # Enforce a static limit for the maximum amount of students that can be enrolled
            override_field_for_ccx(
                ccx_course_object,
                master_course_object,
                'max_student_enrollments_allowed',
                valid_input['max_students_allowed']
            )

            # Hide anything that can show up in the schedule
            hidden = 'visible_to_staff_only'
            for chapter in master_course_object.get_children():
                override_field_for_ccx(ccx_course_object, chapter, hidden, True)
                for sequential in chapter.get_children():
                    override_field_for_ccx(ccx_course_object, sequential, hidden, True)
                    for vertical in sequential.get_children():
                        override_field_for_ccx(ccx_course_object, vertical, hidden, True)

            # make the coach user a coach on the master course
            make_user_coach(coach, master_course_key)

            # pull the ccx course key
            ccx_course_key = CCXLocator.from_course_locator(master_course_object.id, unicode(ccx_course_object.id))
            # enroll the coach in the newly created ccx
            email_params = get_email_params(
                master_course_object,
                auto_enroll=True,
                course_key=ccx_course_key,
                display_name=ccx_course_object.display_name
            )
            enroll_email(
                course_id=ccx_course_key,
                student_email=coach.email,
                auto_enroll=True,
                email_students=True,
                email_params=email_params,
            )
            # assign staff role for the coach to the newly created ccx
            assign_staff_role_to_ccx(ccx_course_key, coach, master_course_object.id)
            # assign staff role for all the staff and instructor of the master course to the newly created ccx
            add_master_course_staff_to_ccx(
                master_course_object,
                ccx_course_key,
                ccx_course_object.display_name,
                send_email=False
            )

        serializer = self.get_serializer(ccx_course_object)

        # using CCX object as sender here.
        responses = SignalHandler.course_published.send(
            sender=ccx_course_object,
            course_key=ccx_course_key
        )
        for rec, response in responses:
            log.info('Signal fired when course is published. Receiver: %s. Response: %s', rec, response)
        return Response(
            status=status.HTTP_201_CREATED,
            data=serializer.data
        )


class CCXDetailView(GenericAPIView):
    """
        **Use Case**

            * Get the details of CCX course.

            * Modify a CCX course.

            * Delete a CCX course.

        **Example Request**

            GET /api/ccx/v0/ccx/{ccx_course_id}

            PATCH /api/ccx/v0/ccx/{ccx_course_id} {

                "display_name": "CCX example title modified",
                "coach_email": "joe@example.com",
                "max_students_allowed": 111,
                "course_modules" : [
                    "block-v1:Organization+EX101+RUN-FALL2099+type@chapter+block@week1",
                    "block-v1:Organization+EX101+RUN-FALL2099+type@chapter+block@week4",
                    "block-v1:Organization+EX101+RUN-FALL2099+type@chapter+block@week5"
                ]
            }

            DELETE /api/ccx/v0/ccx/{ccx_course_id}

        **GET and DELETE Parameters**

            A GET or DELETE request must include the following parameter.

            * ccx_course_id: A string representation of a CCX Course ID.

        **PATCH Parameters**

            A PATCH request can include the following parameters

            * ccx_course_id: A string representation of a CCX Course ID.

            * display_name: Optional. A string representing the CCX Course title.

            * coach_email: Optional. A string representing the CCX owner email.

            * max_students_allowed: Optional. An integer representing he maximum number of students that
              can be enrolled in the CCX Course.

            * course_modules: Optional. A list of course modules id keys.

        **GET Response Values**

            If the request for information about the CCX course is successful, an HTTP 200 "OK" response
            is returned.

            The HTTP 200 response has the following values.

            * ccx_course_id: A string representation of a CCX Course ID.

            * display_name: A string representing the CCX Course title.

            * coach_email: A string representing the CCX owner email.

            * start: A string representing the start date for the CCX Course.

            * due: A string representing the due date for the CCX Course.

            * max_students_allowed: An integer representing he maximum number of students that
              can be enrolled in the CCX Course.

            * course_modules: A list of course modules id keys.

        **PATCH and DELETE Response Values**

            If the request for modification or deletion of a CCX course is successful, an HTTP 204 "No Content"
            response is returned.
    """

    authentication_classes = (
        JwtAuthentication,
        authentication.OAuth2AuthenticationAllowInactiveUser,
        authentication.SessionAuthenticationAllowInactiveUser,
    )
    permission_classes = (IsAuthenticated, permissions.IsCourseStaffInstructor)
    serializer_class = CCXCourseSerializer

    def get_object(self, course_id, is_ccx=False):  # pylint: disable=arguments-differ
        """
        Override the default get_object to allow a custom getter for the CCX
        """
        course_object, course_key, error_code, http_status = get_valid_course(course_id, is_ccx)
        self.check_object_permissions(self.request, course_object)
        return course_object, course_key, error_code, http_status

    def get(self, request, ccx_course_id=None):
        """
        Gets a CCX Course information.

        Args:
            request (Request): Django request object.
            ccx_course_id (string): URI element specifying the CCX course location.

        Return:
            A JSON serialized representation of the CCX course.
        """
        ccx_course_object, _, error_code, http_status = self.get_object(ccx_course_id, is_ccx=True)
        if ccx_course_object is None:
            return Response(
                status=http_status,
                data={
                    'error_code': error_code
                }
            )
        serializer = self.get_serializer(ccx_course_object)
        return Response(serializer.data)

    def delete(self, request, ccx_course_id=None):  # pylint: disable=unused-argument
        """
        Deletes a CCX course.

        Args:
            request (Request): Django request object.
            ccx_course_id (string): URI element specifying the CCX course location.
        """
        ccx_course_object, ccx_course_key, error_code, http_status = self.get_object(ccx_course_id, is_ccx=True)
        if ccx_course_object is None:
            return Response(
                status=http_status,
                data={
                    'error_code': error_code
                }
            )
        ccx_course_overview = CourseOverview.get_from_id(ccx_course_key)
        # clean everything up with a single transaction
        with transaction.atomic():
            CcxFieldOverride.objects.filter(ccx=ccx_course_object).delete()
            # remove all users enrolled in the CCX from the CourseEnrollment model
            CourseEnrollment.objects.filter(course_id=ccx_course_key).delete()
            ccx_course_overview.delete()
            ccx_course_object.delete()
        return Response(
            status=status.HTTP_204_NO_CONTENT,
        )

    def patch(self, request, ccx_course_id=None):
        """
        Modifies a CCX course.

        Args:
            request (Request): Django request object.
            ccx_course_id (string): URI element specifying the CCX course location.
        """
        ccx_course_object, ccx_course_key, error_code, http_status = self.get_object(ccx_course_id, is_ccx=True)
        if ccx_course_object is None:
            return Response(
                status=http_status,
                data={
                    'error_code': error_code
                }
            )

        master_course_id = request.data.get('master_course_id')
        if master_course_id is not None and unicode(ccx_course_object.course_id) != master_course_id:
            return Response(
                status=status.HTTP_403_FORBIDDEN,
                data={
                    'error_code': 'master_course_id_change_not_allowed'
                }
            )

        valid_input, field_errors = get_valid_input(request.data, ignore_missing=True)
        if field_errors:
            return Response(
                status=status.HTTP_400_BAD_REQUEST,
                data={
                    'field_errors': field_errors
                }
            )

        # get the master course key and master course object
        master_course_object, master_course_key, _, _ = get_valid_course(unicode(ccx_course_object.course_id))

        with transaction.atomic():
            # update the display name
            if 'display_name' in valid_input:
                ccx_course_object.display_name = valid_input['display_name']
            # check if the coach has changed and in case update it
            old_coach = None
            if 'coach_email' in valid_input:
                try:
                    coach = User.objects.get(email=valid_input['coach_email'])
                except User.DoesNotExist:
                    return Response(
                        status=status.HTTP_404_NOT_FOUND,
                        data={
                            'error_code': 'coach_user_does_not_exist'
                        }
                    )
                if ccx_course_object.coach.id != coach.id:
                    old_coach = ccx_course_object.coach
                    ccx_course_object.coach = coach
            if 'course_modules' in valid_input:
                if valid_input.get('course_modules'):
                    if not valid_course_modules(valid_input['course_modules'], master_course_key):
                        return Response(
                            status=status.HTTP_400_BAD_REQUEST,
                            data={
                                'error_code': 'course_module_list_not_belonging_to_master_course'
                            }
                        )
                # course_modules to be stored in a json stringified field
                ccx_course_object.structure_json = json.dumps(valid_input.get('course_modules'))
            ccx_course_object.save()
            # update the overridden field for the maximum amount of students
            if 'max_students_allowed' in valid_input:
                override_field_for_ccx(
                    ccx_course_object,
                    ccx_course_object.course,
                    'max_student_enrollments_allowed',
                    valid_input['max_students_allowed']
                )
            # if the coach has changed, update the permissions
            if old_coach is not None:
                # make the new ccx coach a coach on the master course
                make_user_coach(coach, master_course_key)
                # enroll the coach in the ccx
                email_params = get_email_params(
                    master_course_object,
                    auto_enroll=True,
                    course_key=ccx_course_key,
                    display_name=ccx_course_object.display_name
                )
                enroll_email(
                    course_id=ccx_course_key,
                    student_email=coach.email,
                    auto_enroll=True,
                    email_students=True,
                    email_params=email_params,
                )
                # make the new coach staff on the CCX
                assign_staff_role_to_ccx(ccx_course_key, coach, master_course_object.id)

        # using CCX object as sender here.
        responses = SignalHandler.course_published.send(
            sender=ccx_course_object,
            course_key=ccx_course_key
        )
        for rec, response in responses:
            log.info('Signal fired when course is published. Receiver: %s. Response: %s', rec, response)

        return Response(
            status=status.HTTP_204_NO_CONTENT,
        )
