from copy import deepcopy

from django.utils import timezone
from rest_framework import serializers
from wagtail.models import Page

from vbv_lernwelt.assignment.models import (
    Assignment,
    AssignmentCompletion,
    AssignmentCompletionAuditLog,
    AssignmentCompletionStatus,
    is_valid_assignment_completion_status,
)
from vbv_lernwelt.core.models import User
from vbv_lernwelt.core.utils import find_first
from vbv_lernwelt.course.models import CourseCompletionStatus, CourseSession
from vbv_lernwelt.course.services import mark_course_completion
from vbv_lernwelt.notify.services import NotificationService


def update_assignment_completion(
    assignment_user: User,
    assignment: Assignment,
    course_session: CourseSession,
    learning_content_page: Page | None = None,
    completion_data=None,
    completion_status: AssignmentCompletionStatus = AssignmentCompletionStatus.IN_PROGRESS,
    evaluation_user: User | None = None,
    evaluation_points: float | None = None,
    evaluation_passed: bool | None = None,
    evaluation_max_points: float | None = None,
    validate_completion_status_change: bool = True,
    copy_task_data: bool = False,
    initialize_completion: bool = False,
    additional_json_data: dict | None = None,
    edoniq_extended_time_flag: bool = False,
    validate_submission_update: bool = True,
) -> tuple[AssignmentCompletion, bool]:
    """
    :param completion_data: should have the following structure:
       {
         "<user_text_input:uuid>": {"user_data": {"text": "some text from user"}},
         "<user_confirmation:uuid>": {"user_data": {"confirmation": true}},
       }
       every input field has the data stored in sub dict of the question uuid

       it can also contain "trainer_input" when the trainer has entered grading data
       {
         "<user_text_input:uuid>": {
           "expert_data": {"points": 4, "text": "Gute Antwort"}
         },
       }
    :param copy_task_data: if true, the task data will be copied to the completion data
      used for "SUBMITTED" and "EVALUATION_SUBMITTED" status, so that we don't lose the question
      context
    :param initialize_completion: if true, the completion will be created, but not updated
      used as a workaround for initial work with the object on the frontend
    """
    if completion_data is None:
        completion_data = {}

    if additional_json_data is None:
        additional_json_data = {}

    ac, created = AssignmentCompletion.objects.get_or_create(
        assignment_user_id=assignment_user.id,
        assignment_id=assignment.id,
        course_session_id=course_session.id,
        learning_content_page_id=learning_content_page.id
        if learning_content_page
        else None,
    )

    if initialize_completion:
        # ignore further updates
        return ac, True

    if not is_valid_assignment_completion_status(completion_status):
        raise serializers.ValidationError(
            {
                "completion_status": f"Invalid completion status {completion_status.value}"
            }
        )

    if validate_completion_status_change:
        if completion_status == AssignmentCompletionStatus.SUBMITTED:
            completion_status_to_check = [
                "EVALUATION_IN_PROGRESS",
                "EVALUATION_SUBMITTED",
            ]
            if validate_submission_update:
                completion_status_to_check.append("SUBMITTED")
            if ac.completion_status in completion_status_to_check:
                raise serializers.ValidationError(
                    {
                        "completion_status": f"Cannot update completion status from {ac.completion_status} to SUBMITTED"
                    }
                )
        elif (
            completion_status == AssignmentCompletionStatus.EVALUATION_SUBMITTED
            and validate_submission_update
        ):
            if ac.completion_status == "EVALUATION_SUBMITTED":
                raise serializers.ValidationError(
                    {
                        "completion_status": f"Cannot update completion status from {ac.completion_status} to EVALUATION_SUBMITTED"
                    }
                )

        if (
            completion_status == AssignmentCompletionStatus.IN_PROGRESS
            and ac.completion_status != "IN_PROGRESS"
        ):
            raise serializers.ValidationError(
                {
                    "completion_status": f"Cannot set completion status to IN_PROGRESS when it is {ac.completion_status}"
                }
            )

    if completion_status in [
        AssignmentCompletionStatus.EVALUATION_SUBMITTED,
        AssignmentCompletionStatus.EVALUATION_IN_PROGRESS,
    ]:
        if evaluation_user is None:
            raise serializers.ValidationError(
                {
                    "evaluation_user": "evaluation_user is required for EVALUATION_SUBMITTED status"
                }
            )
        ac.evaluation_user = evaluation_user

    if completion_status == AssignmentCompletionStatus.EVALUATION_SUBMITTED:
        if evaluation_points is None:
            raise serializers.ValidationError(
                {
                    "evaluation_points": "evaluation_points is required for EVALUATION_SUBMITTED status"
                }
            )

    if evaluation_max_points is None:
        ac.evaluation_max_points = assignment.get_max_points()
    else:
        ac.evaluation_max_points = evaluation_max_points
    ac.evaluation_points = evaluation_points

    # if no evaluation_passed is provided, we calculate it from the points
    if evaluation_passed is None and ac.evaluation_max_points > 0:
        if evaluation_points is not None and ac.evaluation_max_points is not None:
            # if more or equal than 60% of the points are reached, the assignment is passed
            ac.evaluation_passed = (evaluation_points / ac.evaluation_max_points) >= 0.6
    else:
        ac.evaluation_passed = evaluation_passed

    if completion_status == AssignmentCompletionStatus.SUBMITTED:
        ac.submitted_at = timezone.now()
        if evaluation_user:
            NotificationService.send_assignment_submitted_notification(
                recipient=evaluation_user,
                sender=ac.assignment_user,
                assignment_completion=ac,
            )
    elif completion_status == AssignmentCompletionStatus.EVALUATION_SUBMITTED:
        ac.evaluation_submitted_at = timezone.now()
        learning_content_assignment = (
            learning_content_page
            if learning_content_page
            else assignment.find_attached_learning_content()
        )
        if learning_content_assignment:
            assignment_frontend_url = learning_content_assignment.get_frontend_url()
            NotificationService.send_assignment_evaluated_notification(
                recipient=ac.assignment_user,
                sender=evaluation_user,
                assignment_completion=ac,
                target_url=assignment_frontend_url,
            )

    ac.completion_status = completion_status.value
    ac.edoniq_extended_time_flag = edoniq_extended_time_flag
    ac.additional_json_data = ac.additional_json_data | additional_json_data

    task_ids = [task.id for task in assignment.tasks]

    for key, value in completion_data.items():
        if key in task_ids:
            stored_entry = ac.completion_data.get(key, {})
            stored_entry.update(value)
            ac.completion_data[key] = stored_entry

    # TODO: make more validation of the provided input -> maybe with graphql
    completion_data = _remove_unknown_entries(assignment, completion_data)
    for key, value in completion_data.items():
        # retain already stored data
        stored_entry = ac.completion_data.get(key, {})
        stored_entry.update(value)
        ac.completion_data[key] = stored_entry

    if copy_task_data:
        # copy over the question data, so that we don't lose the context
        sub_tasks = assignment.get_input_tasks()
        for key, value in ac.completion_data.items():
            task_data = find_first(sub_tasks, pred=lambda x: x["id"] == key)
            if task_data:
                ac.completion_data[key].update(task_data)

    ac.save()

    if completion_status in [
        AssignmentCompletionStatus.EVALUATION_SUBMITTED,
        AssignmentCompletionStatus.SUBMITTED,
    ]:
        acl = AssignmentCompletionAuditLog.objects.create(
            assignment_user=assignment_user,
            assignment=assignment,
            course_session=course_session,
            evaluation_user=evaluation_user,
            completion_status=completion_status.value,
            assignment_user_email=assignment_user.email,
            assignment_slug=assignment.slug,
            completion_data=deepcopy(ac.completion_data),
            evaluation_points=evaluation_points,
            evaluation_max_points=ac.evaluation_max_points,
            evaluation_passed=ac.evaluation_passed,
        )
        if evaluation_user:
            acl.evaluation_user_email = evaluation_user.email

        # copy over the question data, so that we don't lose the context
        subtasks = assignment.get_input_tasks()
        for key, value in acl.completion_data.items():
            task_data = find_first(subtasks, pred=lambda x: x["id"] == key)
            if task_data:
                acl.completion_data[key].update(task_data)
        acl.save()

        if completion_status in [
            AssignmentCompletionStatus.EVALUATION_SUBMITTED,
            AssignmentCompletionStatus.EVALUATION_IN_PROGRESS,
            AssignmentCompletionStatus.SUBMITTED,
        ]:
            learning_content = (
                learning_content_page
                if learning_content_page
                else assignment.find_attached_learning_content()
            )
            if learning_content:
                mark_course_completion(
                    user=assignment_user,
                    page=learning_content,
                    course_session=course_session,
                    completion_status=CourseCompletionStatus.SUCCESS.value,
                )

    return ac, created


def _remove_unknown_entries(assignment, completion_data):
    """
    Removes all entries from completion_data which are not known to the assignment
    """
    input_task_ids = [task["id"] for task in assignment.get_input_tasks()]
    filtered_completion_data = {
        key: value for key, value in completion_data.items() if key in input_task_ids
    }
    return filtered_completion_data
