import canvaslms.cli
from canvaslms.cli import assignments, courses, submissions, users
import canvaslms.hacks.canvasapi

import argparse
import csv
import canvasapi.submission
import datetime as dt
import importlib
import importlib.machinery
import importlib.util
import os
import pathlib
import re
import sys


def results_command(config, canvas, args):
    output = csv.writer(sys.stdout, delimiter=args.delimiter)

    if args.assignment_group != "":
        results = summarize_assignment_groups(canvas, args)
    else:
        results = summarize_assignments(canvas, args)

    for result in results:
        output.writerow(result)


def filter_grade(grade, regex):
    """
    Returns True if the grade matches the regex.
    """
    return re.search(regex, grade)


def summarize_assignments(canvas, args):
    """
    Turn submissions into results:
    - canvas is a Canvas object,
    - args is the command-line arguments, as parsed by argparse.
    """

    assignments_list = assignments.process_assignment_option(canvas, args)
    users_list = users.process_user_or_group_option(canvas, args)

    submissions_list = submissions.filter_submissions(
        submissions.list_submissions(assignments_list, include=["submission_history"]),
        users_list,
    )

    for submission in submissions_list:
        if submission.grade is not None:
            if filter_grade(submission.grade, args.filter_grades):
                yield [
                    submission.assignment.course.course_code,
                    submission.assignment.name,
                    submission.user.integration_id,
                    submission.grade,
                    round_to_day(submission.submitted_at or submission.graded_at),
                    *all_graders(submission),
                ]


def round_to_day(timestamp):
    """
    Takes a Canvas timestamp and returns the corresponding datetime.date object.
    """
    return dt.date.fromisoformat(timestamp.split("T")[0])


def all_graders(submission):
    """
    Returns a list of everyone who participated in the grading of the submission.
    I.e. also those who graded previous submissions, when submission history is
    available.
    """
    graders = []

    for prev_submission in submission.submission_history:
        prev_submission = canvasapi.submission.Submission(
            submission._requester, prev_submission
        )
        prev_submission.assignment = submission.assignment
        grader = submissions.resolve_grader(prev_submission)
        if grader:
            graders.append(grader)

    return graders


def summarize_assignment_groups(canvas, args):
    """
    Summarize assignment groups into a single grade:
    - canvas is a Canvas object,
    - args is the command-line arguments, as parsed by argparse.
    """

    courses_list = courses.process_course_option(canvas, args)
    all_assignments = list(assignments.process_assignment_option(canvas, args))
    users_list = set(users.process_user_or_group_option(canvas, args))

    for course in courses_list:
        ag_list = assignments.filter_assignment_groups(course, args.assignment_group)

        for assignment_group in ag_list:
            assignments_list = list(
                assignments.filter_assignments_by_group(
                    assignment_group, all_assignments
                )
            )
            if args.missing:
                if args.missing:
                    try:
                        missing = load_module(args.missing)
                    except Exception as err:
                        canvaslms.cli.err(
                            1,
                            f"Error loading missing module " f"'{args.missing}': {err}",
                        )
                if missing.missing_assignments == missing_assignments:
                    missing_results = missing.missing_assignments(
                        assignments_list,
                        users_list,
                        passing_regex=args.filter_grades,
                        optional_assignments=args.optional_assignments,
                    )
                else:
                    missing_results = missing.missing_assignments(
                        assignments_list, users_list
                    )
                for user, assignment, reason in missing_results:
                    yield [
                        course.course_code,
                        assignment_group.name,
                        user.login_id,
                        assignment.name,
                        reason,
                    ]
            else:
                try:
                    summary = load_module(args.summary_module)
                except Exception as err:
                    canvaslms.cli.err(
                        1,
                        f"Error loading summary module "
                        f"'{args.summary_module}': {err}",
                    )
                for user, grade, grade_date, *graders in summary.summarize_group(
                    assignments_list, users_list
                ):
                    if (
                        grade is None
                        or grade_date is None
                        or not filter_grade(grade, args.filter_grades)
                    ):
                        continue
                    yield [
                        course.course_code,
                        assignment_group.name,
                        user.integration_id,
                        grade,
                        grade_date,
                        *graders,
                    ]


def load_module(module_name):
    """
    Load a module from the file system or a built-in module.
    """
    try:
        return importlib.import_module(module_name)
    except ModuleNotFoundError:
        module_path = pathlib.Path.cwd() / module_name
        module = module_path.stem

        loader = importlib.machinery.SourceFileLoader(module, str(module_path))
        spec = importlib.util.spec_from_loader(module, loader)
        module_obj = importlib.util.module_from_spec(spec)
        loader.exec_module(module_obj)
        return module_obj


def missing_assignments(
    assignments_list,
    users_list,
    passing_regex=r"^([A-EP]|complete)$",
    optional_assignments=None,
):
    """
    Returns tuples of missing assignments.

    For each assignment that a student is not done with, we yield a tuple of the
    user, the assignment and the reason why the assignment is missing.

    The reason can be "not submitted" or "not graded" or "not a passing grade".

    The only reason to use a different module is if you have optional assignments.
    We only want to remind the students of the things they need to pass the course.
    We don't want to make it sound like an optional assignment is mandatory.
    """
    for user in users_list:
        for assignment in assignments_list:
            if optional_assignments:
                if any(
                    re.search(optional, assignment.name)
                    for optional in optional_assignments
                ):
                    continue
            try:
                submission = assignment.get_submission(user)
            except canvasapi.exceptions.ResourceDoesNotExist:
                continue

            if submission is None:
                yield user, assignment, "not submitted"
            elif submission.grade is None:
                if submission.submitted_at:
                    yield user, assignment, f"submitted on {submission.submitted_at}, but not graded"
                else:
                    yield user, assignment, "not done"
            elif not filter_grade(submission.grade, passing_regex):
                if (
                    submission.submitted_at
                    and submission.submitted_at > submission.graded_at
                ):
                    yield user, assignment, f"not a passing grade ({submission.grade}), resubmission not graded"
                else:
                    yield user, assignment, f"not a passing grade ({submission.grade})"


def add_command(subp):
    """Adds the results command to argparse parser subp"""
    results_parser = subp.add_parser(
        "results",
        help="Lists results of a course",
        description="""Lists results of a course for export, for instance to the `ladok report` 
                     command. Output format, CSV:

                       <course code> <component code> <student ID> <grade> <grade date> <graders ...>

                     Can also export a list of missing assignment results (--missing option) that 
                     prevent the student from getting a grade. Output format, CSV:

                       <course code> <component code> <student ID> <missing assignment> <reason>

                     The reason can be "not submitted" or "not graded".""",
        epilog="""If you specify an assignment group, the results of the assignments in that 
                group will be summarized. You can supply your own function for summarizing 
                grades through the -S option. See `pydoc3 canvaslms.grades` for different 
                options.""",
    )
    results_parser.set_defaults(func=results_command)
    assignments.add_assignment_option(results_parser, ungraded=False)
    users.add_user_or_group_option(results_parser)
    passing_regex = r"^([A-EP]|complete)$"
    all_grades_regex = r"^([A-F]x?|(in)?complete)$"
    results_parser.add_argument(
        "-F",
        "--filter-grades",
        required=False,
        action="store",
        nargs="?",
        const=all_grades_regex,
        default=passing_regex,
        help=f"Filter grades. By default we only output "
        f"A--Es and Ps ({passing_regex}. "
        f"If you want to include Fs ({all_grades_regex}), use this option. "
        f"You can also supply an optional regex to this option "
        f"to filter grades based on that.",
    )
    default_summary_module = "canvaslms.grades.conjunctavg"
    results_parser.add_argument(
        "-S",
        "--summary-module",
        required=False,
        default=default_summary_module,
        help=f"""Name of Python module or file containing module to load with a custom 
             summarization function to summarize assignment groups. The default module is 
             part of the `canvaslms` package: `{default_summary_module}`. But it could be 
             any Python file in the file system or other built-in modules. See `pydoc3 
             canvaslms.grades` for alternative modules or how to build your own.""",
    )
    default_missing_module = "canvaslms.cli.results"
    results_parser.add_argument(
        "-M",
        "--missing",
        required=False,
        nargs="?",
        const=default_missing_module,
        default=None,
        help="Produce a list of missing assignments instead of grades. "
        "You can supply a custom module to this option, the module must "
        "contain a "
        "function `missing_assignments(assignments_list, users_list). "
        "The default module checks if all things are graded or submitted. "
        "This option only has effect when working with assignment groups.",
    )
    results_parser.add_argument(
        "-O",
        "--optional-assignments",
        required=False,
        nargs="+",
        default=None,
        help="List of regexes matching optional assignments. The default missing "
        "assignments will treat matching assignments as optional.",
    )
