from dataclasses import dataclass
from enum import StrEnum


class Colour(StrEnum):
    GREEN = "G"
    PURPLE = "P"
    RED = "R"


@dataclass(frozen=True, slots=True)
class Move:
    kind: str  # "pour" or "flip"
    src: int
    dst: int = -1

    def __str__(self) -> str:
        if self.kind == "flip":
            return f"flip({self.src})"
        return f"pour({self.src}->{self.dst})"


@dataclass(frozen=True, slots=True)
class Tube:
    """
    A tube in Dr Eureka.

    balls are stored top -> bottom, as seen when the tube is upright.
    base_up=False: normal playable orientation
    base_up=True: terminal flipped orientation; tube can no longer be used
    """

    balls: tuple[Colour, ...]
    base_up: bool = False
    capacity: int = 4

    def __post_init__(self) -> None:
        if len(self.balls) > self.capacity:
            raise ValueError("Tube exceeds capacity.")

    @property
    def is_empty(self) -> bool:
        return not self.balls

    @property
    def is_full(self) -> bool:
        return len(self.balls) >= self.capacity

    def flip_base_up(self) -> "Tube":
        """
        Flip the tube into its terminal base-up orientation.

        This reverses the visible top-to-bottom ball order.
        Once base-up, it cannot be flipped back or used for pouring.
        """
        if self.base_up:
            raise ValueError("Tube is already base-up.")
        return Tube(
            balls=self.balls[::-1],
            base_up=True,
            capacity=self.capacity,
        )

    def remove_top_ball(self) -> tuple[Colour, "Tube"]:
        if self.base_up:
            raise ValueError("Cannot pour from a base-up tube.")
        if self.is_empty:
            raise ValueError("Cannot pour from an empty tube.")

        ball = self.balls[0]
        new_tube = Tube(
            balls=self.balls[1:],
            base_up=False,
            capacity=self.capacity,
        )
        return ball, new_tube

    def add_ball_on_top(self, ball: Colour) -> "Tube":
        if self.base_up:
            raise ValueError("Cannot pour into a base-up tube.")
        if self.is_full:
            raise ValueError("Cannot pour into a full tube.")

        return Tube(
            balls=(ball, *self.balls),
            base_up=False,
            capacity=self.capacity,
        )

    def __str__(self) -> str:
        orientation = "B" if self.base_up else "U"
        contents = "".join(ball.value for ball in self.balls) or "-"
        return f"{orientation}:{contents}"


@dataclass(frozen=True, slots=True)
class GameState:
    tubes: tuple[Tube, ...]

    def __post_init__(self) -> None:
        capacities = {tube.capacity for tube in self.tubes}
        if len(capacities) > 1:
            raise ValueError("All tubes must have the same capacity.")

    @property
    def tube_capacity(self) -> int:
        return self.tubes[0].capacity if self.tubes else 0

    def pour_one_ball(self, src: int, dst: int) -> "GameState":
        """
        Pour exactly one ball from src to dst.
        """
        if src == dst:
            raise ValueError("Source and destination must be different tubes.")

        source = self.tubes[src]
        dest = self.tubes[dst]

        ball, new_source = source.remove_top_ball()
        new_dest = dest.add_ball_on_top(ball)

        new_tubes = list(self.tubes)
        new_tubes[src] = new_source
        new_tubes[dst] = new_dest
        return GameState(tuple(new_tubes))

    def flip_tube_base_up(self, index: int) -> "GameState":
        tube = self.tubes[index]
        new_tube = tube.flip_base_up()

        new_tubes = list(self.tubes)
        new_tubes[index] = new_tube
        return GameState(tuple(new_tubes))

    def canonical(self) -> tuple[Tube, ...]:
        """
        Canonical representation of the state, ignoring tube positions.
        """
        return tuple(sorted(self.tubes, key=lambda tube: (tube.base_up, tube.balls)))

    def __str__(self) -> str:
        return " | ".join(f"{i}:{tube}" for i, tube in enumerate(self.tubes))


def tube_from_string(spec: str, *, capacity: int = 4) -> Tube:
    """
    Parse a tube from strings like:
      'U:GPR'
      'B:RP'
      'U:-'

    U = upright/playable
    B = base-up/terminal
    """
    orientation, contents = spec.strip().split(":", maxsplit=1)
    orientation = orientation.strip().upper()
    contents = contents.strip().upper()

    if orientation not in {"U", "B"}:
        raise ValueError("Orientation must be 'U' or 'B'.")

    balls: list[Colour] = []
    for char in contents:
        if char in {"-", " "}:
            continue
        balls.append(Colour(char))

    tube = Tube(
        balls=tuple(balls),
        base_up=(orientation == "B"),
        capacity=capacity,
    )
    return tube
