from __future__ import annotations

import csv
import hashlib
import json
from dataclasses import asdict, dataclass
from pathlib import Path

SEATS = ("E", "W", "S", "N")


@dataclass(frozen=True)
class Player:
    player_id: str
    player_name: str
    start_table: int
    start_seat: str


@dataclass(frozen=True)
class Assignment:
    player_id: str
    player_name: str
    round: int
    table: int
    seat: str


def load_profile(path: Path) -> dict:
    profile = json.loads(path.read_text())
    steps = profile.get("steps", {})
    if set(steps) != set(SEATS) or not all(isinstance(steps[s], int) for s in SEATS):
        raise ValueError("Movement profile must define integer E, W, S and N steps")
    return profile


def load_players(path: Path) -> list[Player]:
    with path.open(newline="", encoding="utf-8-sig") as handle:
        rows = list(csv.DictReader(handle))
    required = {"player_id", "player_name", "start_table", "start_seat"}
    if not rows or not required.issubset(rows[0]):
        raise ValueError(f"Roster requires columns: {', '.join(sorted(required))}")
    players = [
        Player(
            player_id=row["player_id"].strip(),
            player_name=row["player_name"].strip(),
            start_table=int(row["start_table"]),
            start_seat=row["start_seat"].strip().upper(),
        )
        for row in rows
    ]
    return players


def validate_roster(players: list[Player], table_count: int) -> None:
    errors: list[str] = []
    ids = [p.player_id for p in players]
    if len(ids) != len(set(ids)):
        errors.append("Player IDs must be unique")
    for player in players:
        if not player.player_id.isdigit() or len(player.player_id) > 3:
            errors.append(f"{player.player_id!r}: Player ID must contain 1-3 digits")
        if not player.player_name:
            errors.append(f"{player.player_id}: player name is blank")
        if player.start_table not in range(1, table_count + 1):
            errors.append(f"{player.player_id}: invalid starting table {player.start_table}")
        if player.start_seat not in SEATS:
            errors.append(f"{player.player_id}: invalid starting seat {player.start_seat!r}")
    occupied = {(p.start_table, p.start_seat) for p in players}
    expected = {(table, seat) for table in range(1, table_count + 1) for seat in SEATS}
    missing = sorted(expected - occupied)
    if len(occupied) != len(players):
        errors.append("Round 1 contains duplicate table/seat assignments")
    if missing:
        errors.append("Round 1 is missing: " + ", ".join(f"T{t}-{s}" for t, s in missing))
    if len(players) != table_count * 4:
        errors.append(f"Expected {table_count * 4} players for {table_count} complete tables; found {len(players)}")
    if errors:
        raise ValueError("\n".join(errors))


def generate_assignments(
    players: list[Player], table_count: int, round_count: int, profile: dict
) -> list[Assignment]:
    if not 1 <= round_count <= 40:
        raise ValueError("Round count must be between 1 and 40")
    validate_roster(players, table_count)
    assignments: list[Assignment] = []
    for player in players:
        step = profile["steps"][player.start_seat]
        for round_no in range(1, round_count + 1):
            table = ((player.start_table - 1 + step * (round_no - 1)) % table_count) + 1
            assignments.append(
                Assignment(player.player_id, player.player_name, round_no, table, player.start_seat)
            )
    validate_assignments(assignments, table_count, round_count)
    return assignments


def validate_assignments(assignments: list[Assignment], table_count: int, round_count: int) -> None:
    errors: list[str] = []
    for round_no in range(1, round_count + 1):
        current = [a for a in assignments if a.round == round_no]
        occupied = {(a.table, a.seat) for a in current}
        expected = {(table, seat) for table in range(1, table_count + 1) for seat in SEATS}
        if len(occupied) != len(current):
            errors.append(f"Round {round_no}: duplicate table/seat assignment")
        missing = sorted(expected - occupied)
        if missing:
            errors.append(
                f"Round {round_no}: missing " + ", ".join(f"T{table}-{seat}" for table, seat in missing)
            )
    if errors:
        raise ValueError("\n".join(errors))


def input_checksum(players: list[Player], table_count: int, round_count: int, profile: dict) -> str:
    payload = {
        "players": [asdict(player) for player in sorted(players, key=lambda p: p.player_id)],
        "table_count": table_count,
        "round_count": round_count,
        "profile": profile,
    }
    return hashlib.sha256(json.dumps(payload, sort_keys=True).encode()).hexdigest()

