# problem3solution.py
#
# ICS H32 Fall 2025
# Exercise Set 3
# INSTRUCTOR SOLUTION

from collections import namedtuple
from pathlib import Path



Student = namedtuple('Student', ['scores', 'grade'])



def _convert_scores(score_words: list[str]) -> list[float]:
    'Converts a sequence of student scores into numbers'
    scores = []

    for score_word in score_words:
        scores.append(float(score_word))

    return scores


assert _convert_scores(['10', '20', '30', '40', '50', '60']) \
    == [10, 20, 30, 40, 50, 60]

assert _convert_scores(['10.5', '20.25', '30.125', '40.75', '50.625', '60.875']) \
    == [10.5, 20.25, 30.125, 40.75, 50.625, 60.875]



def _is_in_range(total_score: int, score_range: tuple[float, float] | tuple[float]) -> bool:
    '''
    Returns True if the total score is in the given score range,
    or False otherwise.
    '''
    return total_score >= score_range[0] and (len(score_range) == 1 or total_score < score_range[1])


assert _is_in_range(150, (100, 200))
assert not _is_in_range(175, (200, 300))
assert not _is_in_range(450, (200, 400))
assert _is_in_range(100, (100, 200))
assert not _is_in_range(200, (100, 200))
assert _is_in_range(150, (100, ))
assert _is_in_range(99999, (200, ))
assert _is_in_range(20, (20, ))
assert not _is_in_range(-1, (0, ))



def _determine_grade(total_score: float, grade_ranges: dict) -> str:
    '''
    Given a total score and a dictionary mapping grades to their
    score ranges, returns the appropriate grade (or None if no
    grade is appropriate).
    '''
    for grade, score_range in grade_ranges.items():
        if _is_in_range(total_score, score_range):
            return grade

    return None


assert _determine_grade(450, {'A': (600, ), 'B': (500, 600), 'C': (400, 500)}) \
    == 'C'
assert _determine_grade(550, {'A': (600, ), 'B': (500, 600), 'C': (400, 500)}) \
    == 'B'
assert _determine_grade(650, {'A': (600, ), 'B': (500, 600), 'C': (400, 500)}) \
    == 'A'
assert _determine_grade(350, {'A': (600, ), 'B': (500, 600), 'C': (400, 500)}) \
    == None



def _can_be_skipped(line: str) -> bool:
    '''
    Returns True if the given line from the input file can be
    skipped, False otherwise.
    '''
    line = line.strip()

    if line == '' or line.startswith('#'):
        return True

    return False


assert _can_be_skipped('# Something or other')
assert _can_be_skipped('     ')
assert _can_be_skipped('     # hello')
assert not _can_be_skipped('thornton')
assert not _can_be_skipped('thornton 10')
assert not _can_be_skipped('thornton 10 20 30 40 50')



def _parse_student(line: str, grade_ranges: dict) -> tuple[str, Student] | tuple[None, None]:
    '''
    Given a line of input from the file and a dictionary mapping
    grades to their score ranges, returns a two-element tuple
    containing the student's UCInetID and a Student namedtuple,
    or (None, None) if the line is one that can be safely skipped.
    '''
    if _can_be_skipped(line):
        return None, None
    
    words = line.split()
    ucinetid = words[0]
    scores = _convert_scores(words[1:])
    total_score = sum(scores)
    grade = _determine_grade(total_score, grade_ranges)

    return ucinetid, Student(scores = scores, grade = grade)


assert _parse_student('someone 10 20 30 40 50 60', {'A': (600, ), 'B': (500, 600), 'F': (0, 500)}) \
    == ('someone', Student(scores = [10, 20, 30, 40, 50, 60], grade = 'F'))
assert _parse_student('noscores', {'A': (600, ), 'B': (500, 600), 'X': (0, 500)}) \
    == ('noscores', Student(scores = [], grade = 'X'))
assert _parse_student('# Something or other', {'A': (0, )}) == (None, None)



def build_grade_report(score_file_path: Path, grade_ranges: dict) -> dict:
    score_file = None

    try:
        score_file = score_file_path.open('r')
        grades = dict()

        for line in score_file:
            ucinetid, student = _parse_student(line, grade_ranges)

            if ucinetid != None:
                grades[ucinetid] = student

        return grades
    finally:
        if score_file != None:
            score_file.close()
