"""A small, dependency-free gradebook application.

Provides a `Student` dataclass and a `Gradebook` class that can register
students, record their scores, and compute per-student averages.
"""

from __future__ import annotations

from dataclasses import dataclass, field


class DuplicateStudentError(Exception):
    """Raised when attempting to add a student ID that already exists."""


class StudentNotFoundError(Exception):
    """Raised when referencing a student ID that hasn't been added."""


class InvalidScoreError(Exception):
    """Raised when a score is outside the valid 0-100 range."""


@dataclass
class Student:
    student_id: str
    name: str
    scores: list = field(default_factory=list)

    def average(self) -> float:
        """Return the mean of recorded scores, or 0.0 if none exist."""
        if not self.scores:
            return 0.0
        return sum(self.scores) / len(self.scores)


class Gradebook:
    """Tracks students and their scores."""

    def __init__(self) -> None:
        self._students: dict[str, Student] = {}

    def add_student(self, student_id: str, name: str) -> Student:
        if student_id in self._students:
            raise DuplicateStudentError(
                f"Student with ID '{student_id}' already exists"
            )
        student = Student(student_id=student_id, name=name)
        self._students[student_id] = student
        return student

    def get_student(self, student_id: str) -> Student:
        try:
            return self._students[student_id]
        except KeyError:
            raise StudentNotFoundError(
                f"No student with ID '{student_id}'"
            ) from None

    def add_score(self, student_id: str, score: float) -> None:
        if not 0 <= score <= 100:
            raise InvalidScoreError(
                f"Score {score!r} is out of range; must be between 0 and 100"
            )
        student = self.get_student(student_id)
        student.scores.append(score)

    def average(self, student_id: str) -> float:
        return self.get_student(student_id).average()

    def all_students(self) -> list:
        return list(self._students.values())


def _demo() -> None:
    gradebook = Gradebook()
    gradebook.add_student("s1", "Alice Johnson")
    gradebook.add_student("s2", "Bob Smith")

    for score in (95, 88, 92):
        gradebook.add_score("s1", score)
    for score in (70, 65, 80, 75):
        gradebook.add_score("s2", score)

    print("Gradebook Summary")
    print("=" * 30)
    for student in gradebook.all_students():
        scores_str = ", ".join(str(s) for s in student.scores)
        print(f"{student.name} (ID: {student.student_id})")
        print(f"  Scores : {scores_str}")
        print(f"  Average: {student.average():.2f}")
        print("-" * 30)


if __name__ == "__main__":
    _demo()
