lib/mobus/stepwise/history.ex

defmodule Mobus.Stepwise.History do
  @moduledoc """
  Structured access to runtime history and trace data.

  The engine maintains two audit trails in the runtime:

    * `:history` — state transition log (`from`, `to`, `event`, `at`)
    * `:trace` — fine-grained execution trace (actions, steps, breakpoints)

  These helpers provide stable read access and controlled append
  without depending on internal list structure.
  """

  @doc "Returns the full history list from the runtime."
  @spec events(map()) :: [map()]
  def events(runtime), do: Map.get(runtime, :history, [])

  @doc "Returns the full trace list from the runtime."
  @spec trace(map()) :: [map()]
  def trace(runtime), do: Map.get(runtime, :trace, [])

  @doc "Appends a custom event to the history."
  @spec append(map(), map()) :: map()
  def append(runtime, %{} = entry) do
    entry = Map.put_new(entry, :at, DateTime.utc_now())
    Map.update(runtime, :history, [entry], &(&1 ++ [entry]))
  end

  @doc "Appends a custom entry to the trace."
  @spec append_trace(map(), map()) :: map()
  def append_trace(runtime, %{} = entry) do
    Map.update(runtime, :trace, [entry], &(&1 ++ [entry]))
  end

  @doc "Returns history entries filtered by event name."
  @spec events_by(map(), atom() | String.t()) :: [map()]
  def events_by(runtime, event_name) do
    runtime
    |> events()
    |> Enum.filter(fn entry ->
      Map.get(entry, :event) == event_name or
        to_string(Map.get(entry, :event)) == to_string(event_name)
    end)
  end

  @doc "Returns trace entries filtered by kind."
  @spec trace_by_kind(map(), atom() | String.t()) :: [map()]
  def trace_by_kind(runtime, kind) do
    runtime
    |> trace()
    |> Enum.filter(fn entry ->
      Map.get(entry, :kind) == kind or
        to_string(Map.get(entry, :kind)) == to_string(kind)
    end)
  end

  @doc "Returns the last history entry, or `nil`."
  @spec last_event(map()) :: map() | nil
  def last_event(runtime), do: runtime |> events() |> List.last()

  @doc "Returns the number of state transitions recorded."
  @spec transition_count(map()) :: non_neg_integer()
  def transition_count(runtime), do: runtime |> events() |> length()
end