from decimal import Decimal, ROUND_DOWN, ROUND_HALF_UP

from django.db import transaction
from django.utils import timezone

from secondary.models import (
    ContinuousAssessmentRecord,
    SecondaryComputationPolicy,
    SecondaryGradeBand,
    UNEBSubmissionBatch,
    UNEBSubmissionItem,
)


COMPUTABLE_CA_STATUSES = {
    ContinuousAssessmentRecord.ModerationStatus.MODERATED,
    ContinuousAssessmentRecord.ModerationStatus.APPROVED,
    ContinuousAssessmentRecord.ModerationStatus.LOCKED,
}


def _to_decimal(value):
    return Decimal(str(value))


def round_score(value, rounding_mode):
    score = _to_decimal(value)
    if rounding_mode == SecondaryComputationPolicy.RoundingMode.ONE_DECIMAL:
        return score.quantize(Decimal("0.1"), rounding=ROUND_HALF_UP)
    if rounding_mode == SecondaryComputationPolicy.RoundingMode.WHOLE_NUMBER:
        return score.quantize(Decimal("1"), rounding=ROUND_HALF_UP)
    if rounding_mode == SecondaryComputationPolicy.RoundingMode.WHOLE_NUMBER_DOWN:
        return score.quantize(Decimal("1"), rounding=ROUND_DOWN)
    return score.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)


def get_policy(section=None, level=SecondaryComputationPolicy.Level.LOWER_SECONDARY, on_date=None):
    policy = SecondaryComputationPolicy.get_active_policy(section=section, level=level, on_date=on_date)
    if policy:
        return policy
    return SecondaryComputationPolicy.objects.filter(is_active=True, level=level).order_by("-effective_from", "-id").first()


def get_ca_records(student, subject, uneb_only=False):
    records = ContinuousAssessmentRecord.objects.select_related("task").filter(
        student=student,
        task__subject=subject,
        moderation_status__in=COMPUTABLE_CA_STATUSES,
    )
    if uneb_only:
        records = records.filter(task__uneb_eligible=True)
    return records


def calculate_ca_score(student, subject, uneb_only=False):
    records = list(get_ca_records(student=student, subject=subject, uneb_only=uneb_only))
    weighted_total = Decimal("0.00")
    total_weight = Decimal("0.00")

    for record in records:
        task = record.task
        if task.max_score <= 0 or task.weight <= 0:
            continue
        normalized_score = (record.effective_score / task.max_score) * Decimal("100.00")
        weighted_total += normalized_score * task.weight
        total_weight += task.weight

    if total_weight == 0:
        return Decimal("0.00"), records

    ca_score = (weighted_total / total_weight).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
    return ca_score, records


def resolve_grade(score, policy):
    if not policy:
        return None
    band = SecondaryGradeBand.objects.filter(
        policy=policy,
        min_score__lte=score,
        max_score__gte=score,
    ).order_by("-min_score", "display_order").first()
    return band


def compute_subject_result(student, subject, exam_score, policy=None):
    policy = policy or get_policy(section=subject.section)
    if not policy:
        raise ValueError("No active secondary computation policy found.")

    ca_score, _records = calculate_ca_score(student=student, subject=subject, uneb_only=False)
    exam_score = _to_decimal(exam_score)

    ca_contribution = (ca_score * policy.ca_weight / Decimal("100")).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
    exam_contribution = (exam_score * policy.exam_weight / Decimal("100")).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
    final_score = round_score(ca_contribution + exam_contribution, policy.rounding_mode)

    grade_band = resolve_grade(final_score, policy)
    return {
        "ca_score": ca_score,
        "exam_score": exam_score,
        "ca_contribution": ca_contribution,
        "exam_contribution": exam_contribution,
        "final_score": final_score,
        "grade": grade_band.grade if grade_band else "N/A",
        "descriptor": grade_band.descriptor if grade_band else "",
        "policy_id": policy.id,
    }


def _item_moderation_status(records):
    if not records:
        return ContinuousAssessmentRecord.ModerationStatus.PENDING

    statuses = {record.moderation_status for record in records}
    if statuses == {ContinuousAssessmentRecord.ModerationStatus.LOCKED}:
        return ContinuousAssessmentRecord.ModerationStatus.LOCKED
    if statuses.issubset(
        {
            ContinuousAssessmentRecord.ModerationStatus.APPROVED,
            ContinuousAssessmentRecord.ModerationStatus.LOCKED,
        }
    ):
        return ContinuousAssessmentRecord.ModerationStatus.APPROVED
    if ContinuousAssessmentRecord.ModerationStatus.REJECTED in statuses:
        return ContinuousAssessmentRecord.ModerationStatus.REJECTED
    return ContinuousAssessmentRecord.ModerationStatus.MODERATED


def upsert_uneb_submission_item(batch, student, subject, policy=None):
    if not isinstance(batch, UNEBSubmissionBatch):
        raise ValueError("batch must be a UNEBSubmissionBatch instance.")

    policy = policy or get_policy(section=batch.section, level=batch.level)
    if not policy:
        raise ValueError("No active policy available for UNEB CA submission.")

    ca_score, records = calculate_ca_score(student=student, subject=subject, uneb_only=True)
    final_ca_mark = round_score(ca_score, policy.rounding_mode)
    moderation_status = _item_moderation_status(records)

    item, _created = UNEBSubmissionItem.objects.update_or_create(
        batch=batch,
        student=student,
        subject=subject,
        defaults={
            "ca_mark": ca_score,
            "final_ca_mark": final_ca_mark,
            "source_task_count": len(records),
            "evidence_count": sum(1 for record in records if record.evidence_reference),
            "moderation_status": moderation_status,
        },
    )
    return item


def build_uneb_batch_items(batch, students, subjects, policy=None):
    created_items = []
    for student in students:
        for subject in subjects:
            if subject.section_id != batch.section_id:
                continue
            created_items.append(
                upsert_uneb_submission_item(
                    batch=batch,
                    student=student,
                    subject=subject,
                    policy=policy,
                )
            )
    return created_items


def lock_uneb_batch(batch, user=None):
    if not isinstance(batch, UNEBSubmissionBatch):
        raise ValueError("batch must be a UNEBSubmissionBatch instance.")

    with transaction.atomic():
        timestamp = timezone.now()
        batch.status = UNEBSubmissionBatch.Status.LOCKED
        batch.locked_at = timestamp
        if user and not batch.submitted_by_id:
            batch.submitted_by = user
            batch.submitted_at = timestamp
        batch.save()

        batch.items.update(is_locked=True)

        for item in batch.items.select_related("student", "subject"):
            ContinuousAssessmentRecord.objects.filter(
                student=item.student,
                task__subject=item.subject,
                task__uneb_eligible=True,
            ).exclude(
                moderation_status=ContinuousAssessmentRecord.ModerationStatus.REJECTED
            ).update(
                is_locked=True,
                moderation_status=ContinuousAssessmentRecord.ModerationStatus.LOCKED,
            )
    return batch
