"""Pure-function weighted-average scoring used by SP2/SP4/SP5.

Why not pull SP2's ``compute_weighted_average`` as-is: the SP2 audit flagged
that it silently averages partial submissions (INC020 contract gap). The
new helper accepts a ``require_all_sections`` flag so callers that *gate
selection* on full scoring (Phase 3 INC020 fix) can ask the helper to
return ``None`` instead of a misleading partial average.

The helper is fully decoupled from any model class — callers pass:
* ``sections`` — iterable of objects with ``.pk`` / ``.weight`` / ``.max_marks``
* ``scores``   — iterable of objects with ``.section_id`` / ``.score``

This means the same function works for SP2 ``RubricSection`` + ``JudgeScore``
and for SP4 ``ChallengeRubricSection`` + ``PitchScore`` without any adapter.
"""
from __future__ import annotations

from collections import defaultdict
from collections.abc import Iterable
from decimal import Decimal
from typing import Protocol


class _Section(Protocol):
    pk: int
    weight: Decimal
    max_marks: int


class _Score(Protocol):
    section_id: int
    score: Decimal


def compute_weighted_average(
    sections: Iterable[_Section],
    scores: Iterable[_Score],
    *,
    require_all_sections: bool = False,
) -> Decimal | None:
    """Return the weighted-average score for a single application as a
    percentage in [0, 100] quantized to 2 decimal places.

    Algorithm:
      1. Group scores by section.
      2. For each section: average across judges, normalise by max_marks,
         multiply by section weight.
      3. Sum across sections, scale to a 0-100 percentage.

    Edge cases:
      * No sections → returns ``Decimal("0")``.
      * A section with zero scores AND ``require_all_sections=True`` →
        returns ``None`` (caller should defer ranking).
      * A section with zero scores AND ``require_all_sections=False`` →
        contributes 0 (matches the current SP2 behaviour for backwards
        compatibility).
      * A section with ``max_marks == 0`` → contributes 0 (defensive).
    """
    section_list = list(sections)
    if not section_list:
        return Decimal("0")

    grouped: dict[int, list[Decimal]] = defaultdict(list)
    for s in scores:
        grouped[s.section_id].append(Decimal(s.score))

    total = Decimal("0")
    for section in section_list:
        bucket = grouped.get(section.pk, [])
        if not bucket:
            if require_all_sections:
                return None
            continue
        if section.max_marks == 0:
            continue
        avg = sum(bucket, Decimal("0")) / Decimal(len(bucket))
        normalised = avg / Decimal(section.max_marks)
        total += normalised * Decimal(section.weight)

    return (total * Decimal("100")).quantize(Decimal("0.01"))
