from __future__ import annotations

import random
from dataclasses import dataclass
from typing import Dict, Set, Tuple


Direction = str
Position = Tuple[int, int]

DIRECTIONS: Dict[Direction, Position] = {
    "N": (0, -1),
    "S": (0, 1),
    "E": (1, 0),
    "W": (-1, 0),
}

OPPOSITE: Dict[Direction, Direction] = {
    "N": "S",
    "S": "N",
    "E": "W",
    "W": "E",
}


@dataclass
class Room:
    """Represents one room in the world grid."""

    pos: Position
    exits: Set[Direction]
    is_spawn: bool = False


class MazeWorld:
    """
    Persistent room world:
    - Spawn room is fixed at (0, 0)
    - First visit to a coordinate generates a room
    - Revisiting a coordinate keeps the exact same room
    """

    def __init__(self, seed: int = 42) -> None:
        self.rng = random.Random(seed)
        self.rooms: Dict[Position, Room] = {}
        self.player_pos: Position = (0, 0)

        # Spawn room is always the same and fully open.
        self.rooms[(0, 0)] = Room(
            pos=(0, 0),
            exits={"N", "S", "E", "W"},
            is_spawn=True,
        )

    def _neighbor_pos(self, pos: Position, direction: Direction) -> Position:
        dx, dy = DIRECTIONS[direction]
        return pos[0] + dx, pos[1] + dy

    def _generate_room(self, pos: Position, forced_entry: Direction) -> Room:
        """
        Generate a new room with:
        - forced entry exit (so you can go back),
        - total exits in [3, 4] for traversal-friendly maps.
        """
        exits = {forced_entry}
        desired_count = self.rng.choice((3, 4))

        candidates = [d for d in DIRECTIONS if d not in exits]
        self.rng.shuffle(candidates)
        for direction in candidates:
            if len(exits) >= desired_count:
                break
            exits.add(direction)

        return Room(pos=pos, exits=exits)

    def move(self, direction: Direction) -> Room:
        direction = direction.upper()
        current = self.rooms[self.player_pos]
        if direction not in current.exits:
            raise ValueError(f"Blocked direction: {direction}")

        new_pos = self._neighbor_pos(self.player_pos, direction)

        if new_pos not in self.rooms:
            # Ensure the newly created room has an exit back to current room.
            entry_back = OPPOSITE[direction]
            self.rooms[new_pos] = self._generate_room(new_pos, forced_entry=entry_back)

        self.player_pos = new_pos
        return self.rooms[self.player_pos]

    def current_room(self) -> Room:
        return self.rooms[self.player_pos]

    def debug_map(self) -> str:
        """Small text snapshot of known rooms and exits."""
        lines = []
        for pos in sorted(self.rooms):
            room = self.rooms[pos]
            marker = "SPAWN" if room.is_spawn else "ROOM "
            exits = "".join(sorted(room.exits))
            player = " <= PLAYER" if pos == self.player_pos else ""
            lines.append(f"{marker} {pos} exits:{exits}{player}")
        return "\n".join(lines)


def main() -> None:
    world = MazeWorld(seed=7)
    print("Prototype labyrinthe persistant")
    print("Commandes: Z Q S D | M (map) | X (quitter)")
    print()

    key_to_direction = {
        "Z": "N",
        "S": "S",
        "D": "E",
        "Q": "W",
    }

    while True:
        room = world.current_room()
        print(f"Salle actuelle: {room.pos} | sorties: {sorted(room.exits)}")
        cmd = input("> ").strip().upper()

        if cmd == "X":
            print("Fin.")
            break
        if cmd == "M":
            print(world.debug_map())
            print()
            continue
        if cmd not in key_to_direction:
            print("Commande invalide.")
            continue

        try:
            world.move(key_to_direction[cmd])
        except ValueError as exc:
            print(exc)


if __name__ == "__main__":
    main()
