"""
Weighted final-grade computation for course results.

Each course is assessed across up to three components — Theory, Practical and
the End-of-Term Exam. For vocational training the practical work carries the
most weight, so the default weighting is:

    Theory 30%  +  Practical 40%  +  Exam 30%

The weighted average is computed from marks where they were entered, falling
back to the midpoint of each letter-grade band where only a letter was
recorded. Weights are re-normalised over whichever components actually exist,
so a course graded on Theory + Practical alone still produces a fair final
grade rather than being penalised for the missing exam.
"""
from .models import Result

# Component weights — must cover every grade_type that should count.
COMPONENT_WEIGHTS = {
    Result.THEORY: 0.30,
    Result.PRACTICAL: 0.40,
    Result.EXAM: 0.30,
}

# Display order for components within a course.
COMPONENT_ORDER = {
    Result.THEORY: 0,
    Result.PRACTICAL: 1,
    Result.EXAM: 2,
}

# Midpoint of each grade band, used when no numeric marks were entered.
GRADE_MIDPOINTS = {
    'A': 90,   # 80–100
    'B': 72,   # 65–79
    'C': 57,   # 50–64
    'D': 45,   # 40–49
    'F': 20,   # 0–39
}

GRADE_COLORS = {
    'A': 'success',
    'B': 'primary',
    'C': 'info',
    'D': 'warning',
    'F': 'danger',
}


def _result_score(result):
    """Numeric score for one result — actual marks, or the grade-band midpoint."""
    if result.marks is not None:
        return result.marks
    return GRADE_MIDPOINTS.get(result.grade, 0)


def letter_for_score(score):
    """Map a 0–100 score back to a letter grade using the portal's scale."""
    if score >= 80:
        return 'A'
    if score >= 65:
        return 'B'
    if score >= 50:
        return 'C'
    if score >= 40:
        return 'D'
    return 'F'


def compute_final(results):
    """
    Given the component results for ONE course in ONE term, return a dict with
    the weighted final marks, letter grade and badge colour — or None if there
    are no gradeable components.
    """
    weighted_sum = 0.0
    total_weight = 0.0
    for r in results:
        weight = COMPONENT_WEIGHTS.get(r.grade_type, 0)
        if not weight:
            continue
        weighted_sum += _result_score(r) * weight
        total_weight += weight

    if not total_weight:
        return None

    final_score = round(weighted_sum / total_weight, 1)
    grade = letter_for_score(final_score)
    return {
        'marks': final_score,
        'grade': grade,
        'color': GRADE_COLORS.get(grade, 'secondary'),
    }


def group_results_by_course(results):
    """
    Build a display structure from a flat iterable of Result objects:

        { academic_year: { term_display: [ course_block, ... ] } }

    where each course_block is:

        { 'course': Course,
          'results': [Result, ...]  (ordered Theory, Practical, Exam),
          'final':   {'marks', 'grade', 'color'} or None }

    Insertion order of years/terms/courses follows the order of the queryset
    passed in, so callers control sorting via their .order_by().
    """
    grouped = {}
    for r in results:
        year = r.academic_year
        term = r.get_term_display()
        course_map = grouped.setdefault(year, {}).setdefault(term, {})
        block = course_map.setdefault(r.course_id, {'course': r.course, 'results': []})
        block['results'].append(r)

    output = {}
    for year, terms in grouped.items():
        output[year] = {}
        for term, course_map in terms.items():
            blocks = []
            for block in course_map.values():
                block['results'].sort(key=lambda x: COMPONENT_ORDER.get(x.grade_type, 99))
                block['final'] = compute_final(block['results'])
                blocks.append(block)
            output[year][term] = blocks
    return output
