# lint-amnesty, pylint: disable=missing-module-docstring
import logging

import dateutil
from pytz import UTC
from rest_framework.generics import GenericAPIView
from rest_framework.response import Response

from cms.djangoapps.contentstore.course_info_model import get_course_updates
from cms.djangoapps.contentstore.views.certificates import CertificateManager
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, view_auth_classes
from xmodule.course_metadata_utils import DEFAULT_GRADING_POLICY
from xmodule.modulestore.django import modulestore

from .utils import course_author_access_required, get_bool_param

log = logging.getLogger(__name__)


@view_auth_classes()
class CourseValidationView(DeveloperErrorViewMixin, GenericAPIView):
    """
    **Use Case**

    **Example Requests**

        GET /api/courses/v1/validation/{course_id}/

    **GET Parameters**

        A GET request may include the following parameters.

        * all
        * dates
        * assignments
        * grades
        * certificates
        * updates
        * proctoring
        * graded_only (boolean) - whether to included graded subsections only in the assignments information.
        * validate_oras (boolean) - whether to check the dates in ORA problems in addition to assignment due dates.

    **GET Response Values**

        The HTTP 200 response has the following values.

        * is_self_paced - whether the course is self-paced.
        * dates
            * has_start_date - whether the start date is set on the course.
            * has_end_date - whether the end date is set on the course.
        * assignments
            * total_number - total number of assignments in the course.
            * total_visible - number of assignments visible to learners in the course.
            * assignments_with_dates_before_start - assignments with due dates before the start date.
            * assignments_with_dates_after_end - assignments with due dates after the end date.
        * grades
            * sum_of_weights - sum of weights for all assignments in the course (valid ones should equal 1).
        * certificates
            * is_activated - whether the certificate is activated for the course.
            * has_certificate - whether the course has a certificate.
        * updates
            * has_update - whether at least one course update exists.
        * proctoring
            * needs_proctoring_escalation_email - whether the course requires a proctoring escalation email
            * has_proctoring_escalation_email - whether the course has a proctoring escalation email

    """
    @course_author_access_required
    def get(self, request, course_key):
        """
        Returns validation information for the given course.
        """
        all_requested = get_bool_param(request, 'all', False)

        store = modulestore()
        with store.bulk_operations(course_key):
            course = store.get_course(course_key, depth=self._required_course_depth(request, all_requested))

            response = dict(
                is_self_paced=course.self_paced,
            )
            if get_bool_param(request, 'dates', all_requested):
                response.update(
                    dates=self._dates_validation(course)
                )
            if get_bool_param(request, 'assignments', all_requested):
                response.update(
                    assignments=self._assignments_validation(course, request)
                )
            if get_bool_param(request, 'grades', all_requested):
                response.update(
                    grades=self._grades_validation(course)
                )
            if get_bool_param(request, 'certificates', all_requested):
                response.update(
                    certificates=self._certificates_validation(course)
                )
            if get_bool_param(request, 'updates', all_requested):
                response.update(
                    updates=self._updates_validation(course, request)
                )
            if get_bool_param(request, 'proctoring', all_requested):
                response.update(
                    proctoring=self._proctoring_validation(course)
                )

        return Response(response)

    def _required_course_depth(self, request, all_requested):
        if get_bool_param(request, 'assignments', all_requested):
            return 2
        else:
            return 0

    def _dates_validation(self, course):
        return dict(
            has_start_date=self._has_start_date(course),
            has_end_date=course.end is not None,
        )

    def _assignments_validation(self, course, request):  # lint-amnesty, pylint: disable=missing-function-docstring
        assignments, visible_assignments = self._get_assignments(course)
        assignments_with_dates = [
            a for a in visible_assignments if a.due
        ]
        assignments_with_dates_before_start = (
            [
                {'id': str(a.location), 'display_name': a.display_name}
                for a in assignments_with_dates
                if a.due < course.start
            ]
            if self._has_start_date(course)
            else []
        )

        assignments_with_dates_after_end = (
            [
                {'id': str(a.location), 'display_name': a.display_name}
                for a in assignments_with_dates
                if a.due > course.end
            ]
            if course.end
            else []
        )

        if get_bool_param(request, 'graded_only', False):
            assignments_with_dates = [
                a
                for a in visible_assignments
                if a.due and a.graded
            ]
            assignments_with_dates_before_start = (
                [
                    {'id': str(a.location), 'display_name': a.display_name}
                    for a in assignments_with_dates
                    if a.due < course.start
                ]
                if self._has_start_date(course)
                else []
            )

            assignments_with_dates_after_end = (
                [
                    {'id': str(a.location), 'display_name': a.display_name}
                    for a in assignments_with_dates
                    if a.due > course.end
                ]
                if course.end
                else []
            )

        assignments_with_ora_dates_before_start = []
        assignments_with_ora_dates_after_end = []
        if get_bool_param(request, 'validate_oras', False):
            # Iterate over all ORAs to find any with dates outside
            # acceptable range
            for ora in self._get_open_responses(
                course,
                get_bool_param(request, 'graded_only', False)
            ):
                if course.start and self._has_date_before_start(ora, course.start):
                    parent_unit = modulestore().get_item(ora.parent)
                    parent_assignment = modulestore().get_item(parent_unit.parent)
                    assignments_with_ora_dates_before_start.append({
                        'id': str(parent_assignment.location),
                        'display_name': parent_assignment.display_name
                    })
                if course.end and self._has_date_after_end(ora, course.end):
                    parent_unit = modulestore().get_item(ora.parent)
                    parent_assignment = modulestore().get_item(parent_unit.parent)
                    assignments_with_ora_dates_after_end.append({
                        'id': str(parent_assignment.location),
                        'display_name': parent_assignment.display_name
                    })

        return dict(
            total_number=len(assignments),
            total_visible=len(visible_assignments),
            assignments_with_dates_before_start=assignments_with_dates_before_start,
            assignments_with_dates_after_end=assignments_with_dates_after_end,
            assignments_with_ora_dates_before_start=assignments_with_ora_dates_before_start,
            assignments_with_ora_dates_after_end=assignments_with_ora_dates_after_end,
        )

    def _grades_validation(self, course):
        has_grading_policy = self._has_grading_policy(course)
        sum_of_weights = course.grader.sum_of_weights
        return dict(
            has_grading_policy=has_grading_policy,
            sum_of_weights=sum_of_weights,
        )

    def _certificates_validation(self, course):
        is_activated, certificates = CertificateManager.is_activated(course)
        certificates_enabled = certificates is not None
        return dict(
            is_activated=is_activated,
            has_certificate=certificates_enabled and len(certificates) > 0,
            is_enabled=certificates_enabled,
        )

    def _updates_validation(self, course, request):
        updates_usage_key = course.id.make_usage_key('course_info', 'updates')
        updates = get_course_updates(updates_usage_key, provided_id=None, user_id=request.user.id)
        return dict(
            has_update=len(updates) > 0,
        )

    def _get_assignments(self, course):  # lint-amnesty, pylint: disable=missing-function-docstring
        store = modulestore()
        sections = [store.get_item(section_usage_key) for section_usage_key in course.children]
        assignments = [
            store.get_item(assignment_usage_key)
            for section in sections
            for assignment_usage_key in section.children
        ]
        visible_sections = [
            s for s in sections
            if not s.visible_to_staff_only and not s.hide_from_toc
        ]
        assignments_in_visible_sections = [
            store.get_item(assignment_usage_key)
            for visible_section in visible_sections
            for assignment_usage_key in visible_section.children
        ]
        visible_assignments = [
            a for a in assignments_in_visible_sections
            if not a.visible_to_staff_only
        ]
        return assignments, visible_assignments

    def _get_open_responses(self, course, graded_only):
        oras = modulestore().get_items(course.id, qualifiers={'category': 'openassessment'})
        return oras if not graded_only else [ora for ora in oras if ora.graded]

    def _has_date_before_start(self, ora, start):  # lint-amnesty, pylint: disable=missing-function-docstring
        if ora.submission_start:
            if dateutil.parser.parse(ora.submission_start).replace(tzinfo=UTC) < start:
                return True
        if ora.submission_due:
            if dateutil.parser.parse(ora.submission_due).replace(tzinfo=UTC) < start:
                return True
        for assessment in ora.rubric_assessments:
            if assessment['start']:
                if dateutil.parser.parse(assessment['start']).replace(tzinfo=UTC) < start:
                    return True
            if assessment['due']:
                if dateutil.parser.parse(assessment['due']).replace(tzinfo=UTC) < start:
                    return True

        return False

    def _has_date_after_end(self, ora, end):  # lint-amnesty, pylint: disable=missing-function-docstring
        if ora.submission_start:
            if dateutil.parser.parse(ora.submission_start).replace(tzinfo=UTC) > end:
                return True
        if ora.submission_due:
            if dateutil.parser.parse(ora.submission_due).replace(tzinfo=UTC) > end:
                return True
        for assessment in ora.rubric_assessments:
            if assessment['start']:
                if dateutil.parser.parse(assessment['start']).replace(tzinfo=UTC) > end:
                    return True
            if assessment['due']:
                if dateutil.parser.parse(assessment['due']).replace(tzinfo=UTC) > end:
                    return True
        return False

    def _has_start_date(self, course):
        return not course.start_date_is_still_default

    def _has_grading_policy(self, course):  # lint-amnesty, pylint: disable=missing-function-docstring
        grading_policy_formatted = {}
        default_grading_policy_formatted = {}

        for grader, assignment_type, weight in course.grader.subgraders:
            grading_policy_formatted[assignment_type] = {
                'type': assignment_type,
                'short_label': grader.short_label,
                'min_count': grader.min_count,
                'drop_count': grader.drop_count,
                'weight': weight,
            }

        # the default grading policy Lab assignment type does not have a short-label,
        # but courses with the default grading policy do return a short-label for Lab
        # assignments, so we ignore the Lab short-label
        if 'Lab' in grading_policy_formatted:
            grading_policy_formatted['Lab'].pop('short_label')

        for assignment in DEFAULT_GRADING_POLICY['GRADER']:
            default_assignment_grading_policy_formatted = {
                'type': assignment['type'],
                'min_count': assignment['min_count'],
                'drop_count': assignment['drop_count'],
                'weight': assignment['weight'],
            }

            # the default grading policy Lab assignment type does not have a short-label, so only
            # add short_label to dictionary when the assignment has one
            if 'short_label' in assignment:
                default_assignment_grading_policy_formatted['short_label'] = assignment['short_label']

            default_grading_policy_formatted[assignment['type']] = default_assignment_grading_policy_formatted

        # check for equality
        if len(grading_policy_formatted) != len(default_grading_policy_formatted):
            return True
        else:
            for assignment_type in grading_policy_formatted:
                if (assignment_type not in default_grading_policy_formatted or
                        grading_policy_formatted[assignment_type] != default_grading_policy_formatted[assignment_type]):
                    return True

        return False

    def _proctoring_validation(self, course):
        # A proctoring escalation email is currently only required for courses using Proctortrack
        return dict(
            needs_proctoring_escalation_email=course.proctoring_provider == 'proctortrack',
            has_proctoring_escalation_email=bool(course.proctoring_escalation_email)
        )
