from __future__ import annotations

from abc import ABC, abstractmethod
from collections.abc import Iterable
from dataclasses import dataclass

from dr_eureka.core import GameState, Move


@dataclass(frozen=True, slots=True)
class SolutionStep:
    move: Move
    state: GameState


@dataclass(frozen=True, slots=True)
class SearchResult:
    """
    Result returned by a solver.

    steps:
        Ordered sequence of moves together with the resulting state after each move.
    visited_states:
        Number of distinct canonical states explored.
    """

    steps: tuple[SolutionStep, ...]
    visited_states: int

    @property
    def moves(self) -> tuple[Move, ...]:
        return tuple(step.move for step in self.steps)

    @property
    def states(self) -> tuple[GameState, ...]:
        return tuple(step.state for step in self.steps)

    @property
    def move_count(self) -> int:
        return len(self.steps)


class DrEurekaSolver(ABC):
    """
    Base class for Dr Eureka solvers.

    Subclasses only need to implement:
      - iter_neighbours()
      - solve()

    The default state key uses GameState.canonical(), which makes tube order
    irrelevant. Override state_key() if a future algorithm needs something else.
    """

    def state_key(self, state: GameState) -> tuple[object, ...]:
        return state.canonical()

    @abstractmethod
    def iter_neighbours(self, state: GameState) -> Iterable[tuple[Move, GameState]]:
        raise NotImplementedError

    @abstractmethod
    def solve(self, start: GameState, goal: GameState) -> SearchResult | None:
        raise NotImplementedError
