from collections import defaultdict
from dataclasses import dataclass
from math import ceil
from typing import Literal

from django.db.models import Count
from ninja.errors import HttpError

from account.models import RoleChoices, User
from task.models import Task, TaskTypeChoices

from .models import Submission

GradebookTaskType = Literal["tutorial", "challenge"]


@dataclass(frozen=True)
class GradebookFilters:
    classname: str
    task_type: GradebookTaskType | None = None
    username: str | None = None
    include_all_tasks: bool = False


def _score(value):
    rounded = round(float(value or 0), 2)
    if rounded == 0:
        return 0.0
    return rounded


def _grade_for_rank(rank: int, student_count: int) -> str:
    if rank <= ceil(0.30 * student_count):
        return "A"
    if rank <= ceil(0.70 * student_count):
        return "B"
    if rank <= ceil(0.90 * student_count):
        return "C"
    if rank <= ceil(0.95 * student_count):
        return "D"
    return "E"


def _assessment_grade_for_rank(rank: int, student_count: int) -> str:
    # 考核等级：A10% B20% C40% D20% E10%，按单题最高分排名
    if rank <= ceil(0.10 * student_count):
        return "A"
    if rank <= ceil(0.30 * student_count):
        return "B"
    if rank <= ceil(0.70 * student_count):
        return "C"
    if rank <= ceil(0.90 * student_count):
        return "D"
    return "E"


# 考核分数：各等级对应的分数段（高分→低分），段内按名次线性铺开
ASSESSMENT_SCORE_BANDS = {
    "A": (92, 100),
    "B": (84, 91),
    "C": (76, 83),
    "D": (68, 75),
    "E": (60, 67),
}


def _assessment_score(index_in_grade: int, grade_size: int, grade: str) -> int:
    low, high = ASSESSMENT_SCORE_BANDS[grade]
    if grade_size <= 1:
        return high
    # index_in_grade 从 0 开始：段内第 1 名拿段顶，最后 1 名拿段底
    return round(high - (index_in_grade / (grade_size - 1)) * (high - low))


def _task_sort_key(task):
    type_order = 0 if task.task_type == TaskTypeChoices.TUTORIAL else 1
    return (type_order, task.display, task.id)


def _task_type_label(task_type: str) -> str:
    return "教程" if task_type == TaskTypeChoices.TUTORIAL else "挑战"


def _csv_number(value):
    if value == "":
        return ""
    number = float(value)
    if number.is_integer():
        return str(int(number))
    return f"{number:.2f}".rstrip("0").rstrip(".")


def _classes():
    return list(
        User.objects.filter(role__in=(RoleChoices.NORMAL, RoleChoices.ADMIN))
        .exclude(classname="")
        .values_list("classname", flat=True)
        .distinct()
        .order_by("classname")
    )


def _task_csv_header(task):
    return f"{_task_type_label(task['task_type'])}{task['display']}-{task['title']}"


def build_gradebook(filters: GradebookFilters):
    classname = filters.classname.strip() if filters.classname else ""
    if not classname:
        raise HttpError(400, "请选择班级")
    if filters.task_type not in (None, "tutorial", "challenge"):
        raise HttpError(400, "无效的任务类型")

    classes = _classes()
    class_students = list(
        User.objects.filter(
            role__in=(RoleChoices.NORMAL, RoleChoices.ADMIN), classname=classname
        )
        .order_by("username", "id")
        .only("id", "username", "classname")
    )
    class_student_ids = [student.id for student in class_students]
    student_count = len(class_student_ids)
    coverage_threshold_count = ceil(student_count * 0.5) if student_count else 0

    task_submission_qs = Submission.objects.filter(user_id__in=class_student_ids)
    if filters.task_type:
        task_submission_qs = task_submission_qs.filter(task__task_type=filters.task_type)

    submitted_counts = {
        row["task_id"]: row["submitted_count"]
        for row in task_submission_qs.values("task_id").annotate(
            submitted_count=Count("user_id", distinct=True)
        )
    }
    task_map = {
        task.id: task
        for task in Task.objects.filter(id__in=submitted_counts.keys()).only(
            "id",
            "display",
            "title",
            "task_type",
        )
    }

    all_tasks = []
    for task in sorted(task_map.values(), key=_task_sort_key):
        submitted_count = submitted_counts[task.id]
        included = student_count > 0 and submitted_count >= coverage_threshold_count
        all_tasks.append(
            {
                "id": task.id,
                "display": task.display,
                "title": task.title,
                "task_type": task.task_type,
                "submitted_count": submitted_count,
                "coverage": _score(submitted_count / student_count)
                if student_count
                else 0.0,
                "included": included,
            }
        )

    tasks = [task for task in all_tasks if filters.include_all_tasks or task["included"]]
    visible_task_ids = [task["id"] for task in tasks]
    included_task_ids = {task["id"] for task in tasks if task["included"]}

    best_by_pair = {}
    if class_student_ids and visible_task_ids:
        submissions = (
            Submission.objects.filter(
                user_id__in=class_student_ids,
                task_id__in=visible_task_ids,
            )
            .select_related("task")
            .only("id", "user_id", "task_id", "score", "created", "task__task_type")
            .order_by("user_id", "task_id", "-score", "-created", "-id")
        )
        for submission in submissions:
            best_by_pair.setdefault((submission.user_id, submission.task_id), submission)

    rows = []
    for student in class_students:
        scores = {}
        tutorial_total = 0.0
        challenge_total = 0.0
        best_submission_score = 0.0
        submitted_task_count = 0
        missing_task_count = 0

        for task in tasks:
            submission = best_by_pair.get((student.id, task["id"]))
            if submission:
                cell_score = _score(submission.score)
                scores[task["id"]] = {
                    "score": cell_score,
                    "submitted": True,
                    "submission_id": submission.id,
                }
            else:
                cell_score = 0.0
                scores[task["id"]] = {
                    "score": 0.0,
                    "submitted": False,
                    "submission_id": None,
                }

            if not task["included"]:
                continue
            if submission:
                submitted_task_count += 1
                if cell_score > best_submission_score:
                    best_submission_score = cell_score
            else:
                missing_task_count += 1
            if task["task_type"] == TaskTypeChoices.TUTORIAL:
                tutorial_total += cell_score
            else:
                challenge_total += cell_score

        tutorial_total = _score(tutorial_total)
        challenge_total = _score(challenge_total)
        total_score = _score(tutorial_total + challenge_total)
        average_score = (
            _score(total_score / len(included_task_ids)) if included_task_ids else None
        )
        rows.append(
            {
                "user_id": student.id,
                "username": student.username,
                "classname": student.classname,
                "rank": 0,
                "grade": "E",
                "assessment_grade": "E",
                "assessment_score": 60,
                "best_submission_score": _score(best_submission_score),
                "scores": scores,
                "tutorial_total": tutorial_total,
                "challenge_total": challenge_total,
                "total_score": total_score,
                "average_score": average_score,
                "submitted_task_count": submitted_task_count,
                "missing_task_count": missing_task_count,
            }
        )

    rows.sort(key=lambda row: (-row["total_score"], row["username"]))
    for index, row in enumerate(rows, start=1):
        row["rank"] = index
        row["grade"] = _grade_for_rank(index, student_count)

    # 考核等级：按单题最高分单独排名后定级（不改变上面的总分排名）
    assessment_order = sorted(
        rows, key=lambda row: (-row["best_submission_score"], row["username"])
    )
    for index, row in enumerate(assessment_order, start=1):
        row["assessment_grade"] = _assessment_grade_for_rank(index, student_count)

    # 考核分数：在各等级段内按名次（沿用单题最高分排序）线性铺开到对应分数段
    grade_groups = defaultdict(list)
    for row in assessment_order:
        grade_groups[row["assessment_grade"]].append(row)
    for grade, group in grade_groups.items():
        grade_size = len(group)
        for index_in_grade, row in enumerate(group):
            row["assessment_score"] = _assessment_score(
                index_in_grade, grade_size, grade
            )

    username = filters.username.strip().lower() if filters.username else ""
    if username:
        rows = [row for row in rows if username in row["username"].lower()]

    return {
        "classname": classname,
        "classes": classes,
        "task_count": len(tasks),
        "included_task_count": len(included_task_ids),
        "student_count": student_count,
        "coverage_threshold_count": coverage_threshold_count,
        "tasks": tasks,
        "rows": rows,
    }


def gradebook_csv_rows(gradebook):
    tasks = gradebook["tasks"]
    yield [
        "排名",
        "等级",
        "考核等级",
        "考核分数",
        "用户名",
        "班级",
        *[_task_csv_header(task) for task in tasks],
        "教程合计",
        "挑战合计",
        "总分",
        "平均分",
        "已提交任务数",
        "未提交任务数",
    ]

    for row in gradebook["rows"]:
        yield [
            row["rank"],
            row["grade"],
            row["assessment_grade"],
            row["assessment_score"],
            row["username"],
            row["classname"],
            *[_csv_number(row["scores"][task["id"]]["score"]) for task in tasks],
            _csv_number(row["tutorial_total"]),
            _csv_number(row["challenge_total"]),
            _csv_number(row["total_score"]),
            "" if row["average_score"] is None else _csv_number(row["average_score"]),
            row["submitted_task_count"],
            row["missing_task_count"],
        ]
