#!/usr/bin/env python3
"""Validate a Beyond Brute Force Project Selection project.json (Python 3.9+).

Usage: python3 check_project_selection.py [path/to/project.json]
Checks the JSON format only; no repository is needed for Project Selection.
"""
import json
import sys
from pathlib import Path

PROJECTS = (
    "traveling_salesperson",
    "minimum_graph_coloring",
    "minimum_vertex_cover",
    "longest_path",
    "maximum_clique",
)
EXPECTED_KEYS = frozenset({
    "team_name", "team_members", "project_preferences", "assigned_problem"
})
MAX_BYTES = 65536


class InvalidJSON(ValueError):
    pass


def unique_keys(pairs):
    result = {}
    for key, value in pairs:
        if key in result:
            raise InvalidJSON(f"duplicate JSON key: {key!r}")
        result[key] = value
    return result


def read_json(path):
    path = Path(path)
    if not path.is_file():
        raise InvalidJSON(f"missing file: {path.name} (submit a file named project.json)")
    if path.stat().st_size > MAX_BYTES:
        raise InvalidJSON(f"project.json exceeds {MAX_BYTES:,} bytes")
    try:
        content = path.read_text(encoding="utf-8-sig")
        return json.loads(content, object_pairs_hook=unique_keys)
    except (UnicodeError, OSError, json.JSONDecodeError) as exc:
        raise InvalidJSON(f"cannot read valid UTF-8 JSON: {exc}") from exc


def validate(data):
    """Return errors in team, preferences, and state (rubric: 2 / 2 / 1)."""
    issues = {"team": [], "preferences": [], "state": []}
    if not isinstance(data, dict):
        for errors in issues.values():
            errors.append("project.json must contain a JSON object at the top level")
        return issues

    missing = EXPECTED_KEYS - data.keys()
    extra = data.keys() - EXPECTED_KEYS
    if missing:
        issues["state"].append("missing required fields: " + ", ".join(sorted(missing)))
    if extra:
        issues["state"].append("unexpected fields: " + ", ".join(sorted(extra)))

    team_name = data.get("team_name")
    if not isinstance(team_name, str) or not team_name.strip():
        issues["team"].append("team_name must be a non-empty string")
    elif len(team_name.strip()) > 80:
        issues["team"].append("team_name must be no more than 80 characters")

    members = data.get("team_members")
    if not isinstance(members, list) or len(members) not in (1, 2, 3):
        issues["team"].append("team_members must contain 1, 2, or 3 students")
    else:
        names = []
        for i, member in enumerate(members, start=1):
            if not isinstance(member, dict):
                issues["team"].append(f"team_members[{i-1}] must be an object with a name field")
                continue
            if set(member) != {"name"}:
                issues["team"].append(f"team_members[{i-1}] must contain only the field name")
            name = member.get("name")
            if not isinstance(name, str) or not name.strip():
                issues["team"].append(f"member {i}: name must be a non-empty string")
            elif len(name.strip()) > 100:
                issues["team"].append(f"member {i}: name is too long (max 100 characters)")
            else:
                names.append(" ".join(name.split()).casefold())
        if len(names) != len(set(names)):
            issues["team"].append("student names must be distinct within the team")

    prefs = data.get("project_preferences")
    if not isinstance(prefs, list) or len(prefs) != 3:
        issues["preferences"].append("project_preferences must contain exactly 3 ranked choices")
    else:
        invalid = [f"#{i}: {repr(value)}" for i, value in enumerate(prefs, 1)
                   if not isinstance(value, str) or value not in PROJECTS]
        if invalid:
            issues["preferences"].append("invalid project identifiers: " + ", ".join(invalid))
        if all(isinstance(v, str) for v in prefs) and len(set(prefs)) != 3:
            issues["preferences"].append("the 3 project preferences must be distinct")

    if data.get("assigned_problem") != "":
        issues["state"].append('assigned_problem must be the empty string "" until assignments are announced')
    return issues


def check(path):
    try:
        return validate(read_json(path))
    except InvalidJSON as exc:
        return {"team": [str(exc)], "preferences": [str(exc)], "state": [str(exc)]}


def main(argv=None):
    import argparse
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("filename", nargs="?", default="project.json")
    args = parser.parse_args(argv)
    issues = check(args.filename)
    for key, label in (("team", "Team information"),
                       ("preferences", "Project preferences"),
                       ("state", "Submission format/state")):
        if issues[key]:
            print(f"FAIL — {label}:")
            for issue in issues[key]:
                print(f"  - {issue}")
        else:
            print(f"PASS — {label}")
    if all(not v for v in issues.values()):
        print("\nPASS: project.json is ready to submit to Gradescope.")
        return 0
    print("\nPlease correct the errors and re-run this checker before submitting.")
    print("Allowed projects: " + ", ".join(PROJECTS))
    return 1


if __name__ == "__main__":
    sys.exit(main())
