"""Thread activity state machine.

Pure state machine with observer pattern — no I/O, no async, no imports
beyond stdlib.  Core owns the states; adapters (sidecar, server) own
concurrency and transport.
"""

from __future__ import annotations

from enum import Enum
from typing import Callable


class ThreadState(str, Enum):
    """Observable thread activity states."""

    IDLE = "idle"
    QUEUED = "queued"
    STREAMING = "streaming"
    HITL = "hitl"
    DONE = "done"
    ERROR = "error"


VALID_TRANSITIONS: dict[ThreadState, set[ThreadState]] = {
    ThreadState.IDLE:      {ThreadState.QUEUED, ThreadState.STREAMING},
    ThreadState.QUEUED:    {ThreadState.STREAMING, ThreadState.IDLE},
    ThreadState.STREAMING: {
        ThreadState.HITL, ThreadState.DONE, ThreadState.ERROR, ThreadState.IDLE,
    },
    ThreadState.HITL:      {ThreadState.STREAMING, ThreadState.IDLE},
    ThreadState.DONE:      {ThreadState.IDLE},
    ThreadState.ERROR:     {ThreadState.IDLE},
}


class ThreadStateManager:
    """Manages thread states with validated transitions and observer notifications."""

    def __init__(self) -> None:
        self._states: dict[str, ThreadState] = {}
        self._listeners: list[Callable[[str, ThreadState, ThreadState], None]] = []

    def set_state(self, thread_id: str, state: ThreadState) -> ThreadState | None:
        """Set state, validate transition, notify listeners.

        Returns the old state, or None if unchanged (idle->idle no-op).
        Raises ValueError on invalid transition.
        """
        old = self._states.get(thread_id, ThreadState.IDLE)

        if old == state:
            return None  # no-op

        allowed = VALID_TRANSITIONS.get(old, set())
        if state not in allowed:
            raise ValueError(
                f"Invalid transition: {old.value} -> {state.value} "
                f"for thread {thread_id}"
            )

        if state == ThreadState.IDLE:
            self._states.pop(thread_id, None)
        else:
            self._states[thread_id] = state

        for listener in self._listeners:
            listener(thread_id, old, state)

        return old

    def force_idle(self, thread_id: str) -> None:
        """Cancel — force any state to idle, bypassing validation."""
        old = self._states.pop(thread_id, ThreadState.IDLE)
        if old != ThreadState.IDLE:
            for listener in self._listeners:
                listener(thread_id, old, ThreadState.IDLE)

    def get_state(self, thread_id: str) -> ThreadState:
        """Returns IDLE if thread not tracked."""
        return self._states.get(thread_id, ThreadState.IDLE)

    def get_all_active(self) -> dict[str, ThreadState]:
        """All threads with non-idle state."""
        return dict(self._states)

    def clear(self, thread_id: str) -> None:
        """Remove thread from tracking entirely.

        Does not notify listeners — use ``force_idle()`` for observable
        state changes (e.g. cancellation).  ``clear()`` is for silent
        cleanup when the thread is being deleted.
        """
        self._states.pop(thread_id, None)

    def on_change(
        self, callback: Callable[[str, ThreadState, ThreadState], None]
    ) -> Callable[[], None]:
        """Register listener. Returns unsubscribe function."""
        self._listeners.append(callback)

        def unsubscribe() -> None:
            try:
                self._listeners.remove(callback)
            except ValueError:
                pass

        return unsubscribe
