lib/mobus/stepwise/components/stepwise_projection.ex

defmodule Mobus.Stepwise.Components.StepwiseProjection do
  @moduledoc """
  Projection component for stepwise workflows.

  Produces canonical `Mobus.Stepwise.Projection` from the current runtime state.
  """

  alias Mobus.Stepwise.Projection
  alias Mobus.Stepwise.ProjectionHelpers
  alias Mobus.Stepwise.SpecHelpers

  @doc """
  Builds the canonical `Mobus.Stepwise.Projection` from the current runtime.

  This is an ALF pipeline stage. Computes the projection struct containing
  all information needed for UI rendering: current state, available events,
  UI descriptor, artifacts, blocked reasons, subscriptions, errors, and trace.

  Available events are derived from the step's position in the ordered step list:
  - First step: `[:next]`
  - Last step: `[:back]`
  - Middle steps: `[:back, :next]`

  ## Parameters

    * `event` — pipeline event map with `:spec` and `:runtime`
    * `opts` — ALF stage options (unused)

  ## Returns

    * Updated event map with `%Mobus.Stepwise.Projection{}` in both
      `event.runtime.projection` and `event.projection`.

  """
  @spec call(map(), map()) :: map()
  def call(%{spec: spec, runtime: runtime} = event, _opts) do
    current = Map.get(runtime, :current_state)
    ordered = SpecHelpers.ordered_steps(spec)

    projection = %Projection{
      execution_id: Map.fetch!(runtime, :execution_id),
      profile: :stepwise,
      current_state: current,
      available_events: available_events(ordered, current),
      blocked_reasons: Map.get(runtime, :blocked_reasons, %{}),
      breakpoint_hits: Map.get(runtime, :breakpoint_hits, []),
      subscriptions: ProjectionHelpers.subscriptions_for(spec, runtime),
      artifacts: Map.get(runtime, :artifacts, %{}),
      ui: ProjectionHelpers.ui_for(spec, current, runtime),
      errors: Map.get(runtime, :errors, []),
      trace: Map.get(runtime, :trace, []),
      extensions: ProjectionHelpers.build_extensions(spec, runtime)
    }

    runtime = Map.put(runtime, :projection, projection)
    event |> Map.put(:runtime, runtime) |> Map.put(:projection, projection)
  end

  defp available_events(ordered, current) when is_list(ordered) do
    idx = Enum.find_index(ordered, &SpecHelpers.equivalent_state?(&1, current))

    cond do
      is_nil(idx) ->
        []

      idx == 0 and idx + 1 < length(ordered) ->
        [:next]

      idx + 1 >= length(ordered) and idx - 1 >= 0 ->
        [:back]

      idx + 1 < length(ordered) and idx - 1 >= 0 ->
        [:back, :next]

      true ->
        []
    end
  end

  defp available_events(_ordered, _current), do: []
end