"""Node state machine for cluster lifecycle.

States:
  Solo     — standalone mode, SQLite only, no cluster
  Joining  — discovered a hub, handshake in progress
  Spoke    — active cluster member, syncing with hub
  Leaving  — graceful departure in progress
  Error    — join/sync failure, needs recovery

Valid transitions:
  Solo    → Joining  (user initiates join)
  Joining → Spoke    (join protocol completes)
  Joining → Solo     (join fails or cancelled)
  Joining → Error    (join protocol error)
  Spoke   → Leaving  (user initiates leave)
  Spoke   → Error    (sync/connection failure)
  Leaving → Solo     (departure completes)
  Error   → Solo     (manual recovery)
  Error   → Joining  (retry join)
"""
from __future__ import annotations

import logging
from enum import Enum
from typing import Any, Callable

logger = logging.getLogger(__name__)


class NodeState(str, Enum):
    """Possible states for a cluster node."""
    SOLO = "solo"
    JOINING = "joining"
    SPOKE = "spoke"
    LEAVING = "leaving"
    ERROR = "error"


# Valid state transitions
_TRANSITIONS: dict[NodeState, set[NodeState]] = {
    NodeState.SOLO: {NodeState.JOINING},
    NodeState.JOINING: {NodeState.SPOKE, NodeState.SOLO, NodeState.ERROR},
    NodeState.SPOKE: {NodeState.LEAVING, NodeState.ERROR},
    NodeState.LEAVING: {NodeState.SOLO},
    NodeState.ERROR: {NodeState.SOLO, NodeState.JOINING},
}


class InvalidTransitionError(Exception):
    """Raised when a state transition is not valid."""
    pass


class NodeStateMachine:
    """Manages node state transitions with event callbacks.

    Callbacks are fired on state entry and exit, enabling
    components to react (e.g., start/stop sync scheduler).
    """

    def __init__(self, initial_state: NodeState = NodeState.SOLO):
        self._state = initial_state
        self._on_enter: dict[NodeState, list[Callable]] = {s: [] for s in NodeState}
        self._on_exit: dict[NodeState, list[Callable]] = {s: [] for s in NodeState}
        self._on_any_transition: list[Callable] = []
        self._history: list[tuple[NodeState, NodeState]] = []
        self._error_info: str = ""

    @property
    def state(self) -> NodeState:
        return self._state

    @property
    def is_clustered(self) -> bool:
        return self._state in (NodeState.SPOKE, NodeState.JOINING, NodeState.LEAVING)

    @property
    def history(self) -> list[tuple[NodeState, NodeState]]:
        return list(self._history)

    @property
    def error_info(self) -> str:
        return self._error_info

    def can_transition(self, target: NodeState) -> bool:
        """Check if a transition to the target state is valid."""
        return target in _TRANSITIONS.get(self._state, set())

    def transition(self, target: NodeState, *, error_info: str = "") -> None:
        """Transition to a new state.

        Raises InvalidTransitionError if the transition is not valid.
        Fires on_exit callbacks for current state and on_enter for target.
        """
        if not self.can_transition(target):
            raise InvalidTransitionError(
                f"Cannot transition from {self._state.value} to {target.value}"
            )

        old_state = self._state

        # Fire exit callbacks
        for cb in self._on_exit[old_state]:
            try:
                cb(old_state, target)
            except Exception:
                logger.exception("on_exit callback failed for %s", old_state)

        self._state = target
        self._history.append((old_state, target))

        if target == NodeState.ERROR:
            self._error_info = error_info
        else:
            self._error_info = ""

        # Fire enter callbacks
        for cb in self._on_enter[target]:
            try:
                cb(old_state, target)
            except Exception:
                logger.exception("on_enter callback failed for %s", target)

        # Fire any-transition callbacks
        for cb in self._on_any_transition:
            try:
                cb(old_state, target)
            except Exception:
                logger.exception("on_any_transition callback failed")

        logger.info("State: %s → %s", old_state.value, target.value)

    def on_enter(self, state: NodeState, callback: Callable) -> None:
        """Register a callback for when entering a state."""
        self._on_enter[state].append(callback)

    def on_exit(self, state: NodeState, callback: Callable) -> None:
        """Register a callback for when leaving a state."""
        self._on_exit[state].append(callback)

    def on_any_transition(self, callback: Callable) -> None:
        """Register a callback for any state transition."""
        self._on_any_transition.append(callback)

    def to_dict(self) -> dict[str, Any]:
        """Serialize current state for API/persistence."""
        return {
            "state": self._state.value,
            "is_clustered": self.is_clustered,
            "error_info": self._error_info,
            "history_len": len(self._history),
        }
