lib/mobus/stepwise/components/fsm_breakpoint.ex

defmodule Mobus.Stepwise.Components.FsmBreakpoint do
  @moduledoc """
  Breakpoint component for stepwise workflows.

  Records breakpoint hits for debugging and inspection purposes.

  > **Naming note**: Despite the `FsmBreakpoint` module name, this component
  > operates exclusively within stepwise pipelines. The name is historical
  > (predating the profile extraction) and is retained to avoid breaking
  > internal pipeline references.
  """

  alias Mobus.Stepwise.SpecHelpers

  @doc """
  Records breakpoint hits for debugging and inspection.

  This is an ALF pipeline stage. Checks the spec's `:breakpoints` list against
  the current event and state. Matching breakpoints are appended to
  `runtime.breakpoint_hits` with a timestamp, and a trace entry is recorded.

  Supports breakpoint types:
  - `on: :event` — matches when the breakpoint value equals the event name
  - `on: :state_enter` — matches when the breakpoint value equals the current state

  Skipped when the event has an error status.

  ## Parameters

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

  ## Returns

    * Updated event map with breakpoint hits and trace entries in runtime.

  """
  @spec call(map(), map()) :: map()
  def call(%{status: :error} = event, _opts), do: event

  def call(%{spec: spec, runtime: runtime, event: event_name} = event, _opts) do
    breakpoints = Map.get(spec, :breakpoints) || Map.get(spec, "breakpoints") || []
    current = Map.get(runtime, :current_state)

    hits =
      Enum.filter(breakpoints, fn bp ->
        on = Map.get(bp, :on) || Map.get(bp, "on")
        value = Map.get(bp, :value) || Map.get(bp, "value")

        case on do
          :event -> value == event_name or to_string(value) == to_string(event_name)
          "event" -> to_string(value) == to_string(event_name)
          :state_enter -> SpecHelpers.equivalent_state?(value, current)
          "state_enter" -> SpecHelpers.equivalent_state?(value, current)
          _ -> false
        end
      end)

    runtime =
      if hits == [] do
        runtime
      else
        runtime
        |> Map.update(:breakpoint_hits, [], fn existing ->
          existing ++ Enum.map(hits, &Map.put(&1, :hit_at, DateTime.utc_now()))
        end)
        |> Map.update(:trace, [], fn trace ->
          trace ++ [%{kind: :breakpoints, hits: length(hits), event: event_name, state: current}]
        end)
      end

    Map.put(event, :runtime, runtime)
  end

  def call(event, _opts), do: event
end