from __future__ import annotations

import inspect
from datetime import datetime
from typing import Any, Callable, Coroutine, Optional, Sequence

from communication.entries import RpcBundle, ActionEntry
from communication.errors import ConnectError
from state_machine.core import StateMachine
from state_machine.utils import (
    StateResponse,
    HistoryResponse,
    HistoryEntry,
    TriggerResponse,
)


__all__ = ["build_state_machine_rpc_bundle"]


def build_state_machine_rpc_bundle(
    state_machine: StateMachine,
    *,
    include_state_actions: bool = True,
    include_history_action: bool = True,
    triggers: Optional[Sequence[str]] = None,
) -> RpcBundle:
    """
    Build and return an RpcBundle exposing a StateMachine instance.

    Provides unary RPCs:

      - GetState
      - GetHistory
      - Trigger_<TriggerName> (one per trigger)

    For integration via app.extend_bundle(bundle).
    """
    bundle = RpcBundle()

    # ======================================================
    # GetState
    # ======================================================
    if include_state_actions:

        async def get_state() -> StateResponse:
            last = getattr(state_machine, "get_last_state", lambda: None)()
            return StateResponse(state=state_machine.state, last_state=last)

        bundle.actions.append(
            ActionEntry(
                name="GetState",
                func=get_state,
                input_type=None,
                output_type=StateResponse,
            )
        )

    # ======================================================
    # GetHistory
    # ======================================================
    if include_history_action:

        async def get_history() -> HistoryResponse:
            raw = getattr(state_machine, "history", [])
            out = []

            for item in raw:
                if isinstance(item, dict):
                    state_str = str(item.get("state", ""))
                    duration_ms: Optional[int] = item.get("duration_ms")
                    if duration_ms is not None and not isinstance(duration_ms, int):
                        duration_ms = None
                    timestamp: datetime
                    if "timestamp" in item and isinstance(item["timestamp"], datetime):
                        timestamp = item["timestamp"]
                    else:
                        timestamp = datetime.now()
                    out.append(
                        HistoryEntry(
                            state=state_str,
                            timestamp=timestamp,
                            duration_ms=duration_ms,
                        )
                    )
                else:
                    out.append(HistoryEntry(state=str(item), timestamp=datetime.now()))

            return HistoryResponse(history=out, buffer_size=len(raw))

        bundle.actions.append(
            ActionEntry(
                name="GetHistory",
                func=get_history,
                input_type=None,
                output_type=HistoryResponse,
            )
        )

    # ======================================================
    # Triggers
    # ======================================================
    # Source of truth: transitions registered on state machine
    all_triggers = sorted(state_machine.events.keys())
    selected = all_triggers if triggers is None else list(triggers)

    # ---------- factory function (fixes closure bug) ----------
    def make_trigger_handler(
        trigger_name: str,
    ) -> Callable[[], Coroutine[Any, Any, TriggerResponse]]:
        async def trigger_call() -> TriggerResponse:
            current = state_machine.state
            allowed = state_machine.get_triggers(current)

            # Precondition error if invalid in current state
            if trigger_name not in allowed:
                raise ConnectError(
                    code="failed_precondition",
                    message=(f"Trigger '{trigger_name}' cannot run from '{current}'. " f"Allowed: {sorted(allowed)}"),
                )

            # Fetch bound trigger method
            try:
                method = getattr(state_machine, trigger_name)
            except AttributeError as e:
                raise ConnectError(
                    code="internal",
                    message=f"StateMachine missing trigger '{trigger_name}'",
                ) from e

            # Execute trigger (sync or async)
            try:
                result = method()
                if inspect.isawaitable(result):
                    await result
            except Exception as e:
                raise ConnectError(
                    code="internal",
                    message=f"Trigger '{trigger_name}' failed: {e}",
                ) from e

            return TriggerResponse(
                result=trigger_name,
                previous_state=current,
                new_state=state_machine.state,
            )

        return trigger_call

    # ---------- register each trigger ----------
    for trig in selected:
        rpc_name = f"Trigger_{trig[0].upper()}{trig[1:]}"  # PascalCase RPC names
        handler = make_trigger_handler(trig)

        bundle.actions.append(
            ActionEntry(
                name=rpc_name,
                func=handler,
                input_type=None,
                output_type=TriggerResponse,
            )
        )

    return bundle
