defmodule RefugeWeb.InvocationLive do
  @moduledoc """
  Shows details for a single Bazel invocation / build.
  Mimics the behaviour of BuildBuddy's invocation page.
  Currently supports live streaming of build-event protocol (BEP) events that
  belong to the given `invocation_id` (which we treat as the build_id emitted
  in lifecycle events) coming from `Refuge.GRPC.EventStore`.
  """

  use RefugeWeb, :live_view
  require Logger

  @impl true
  def mount(%{"id" => invocation_id}, _session, socket) do
    # Subscribe to the shared gRPC event store so we get events.
    :ok = GenServer.call(Refuge.GRPC.EventStore, :subscribe)

    {:ok,
     assign(socket,
       invocation_id: invocation_id,
       page_title: "Invocation #{invocation_id}",
       build_events: [],
       streaming: true
     )}
  end

  ###############################################
  # EventStore messages
  ###############################################

  # Called when EventStore forwards JSON-encoded string
  @impl true
  def handle_info({:new_build_event, event}, socket) when is_binary(event) do
    with {:ok, decoded} <- Jason.decode(event) do
      handle_new_event(decoded, socket)
    else
      _ -> {:noreply, socket}
    end
  end

  # Called when EventStore forwards already-decoded map
  @impl true
  def handle_info({:new_build_event, event}, socket) when is_map(event) do
    handle_new_event(event, socket)
  end

  # Ignore unrelated messages
  @impl true
  def handle_info(_, socket), do: {:noreply, socket}

  ###############################################
  defp handle_new_event(event, socket) do
    # Filter events for this invocation ID (matches build_id or invocation_id)
    inv_id = socket.assigns.invocation_id
    stream = get_in(event, ["stream_id"]) || %{}

    formatted = format_event(event)
    events = [formatted | socket.assigns.build_events] |> Enum.take(200)
    {:noreply, assign(socket, build_events: events)}
  end

  defp format_event(event) do
    ts = event["timestamp"] || DateTime.utc_now() |> DateTime.to_string()
    etype = Map.get(event, "event_type", Map.get(event, "eventType", "unknown"))
    
    # Convert the event to a format that can be safely encoded to JSON
    encodable_event = event |> sanitize_for_json()

    """
    [#{ts}] #{etype}
    ##
    #{Jason.encode!(encodable_event, pretty: true)}
    """
  end

  # Recursively process map and list structures to make them JSON-encodable
  # by converting tuples and any other non-encodable structures
  defp sanitize_for_json(nil), do: nil
  defp sanitize_for_json(data) when is_map(data) do
    data
    |> Enum.map(fn {k, v} -> {k, sanitize_for_json(v)} end)
    |> Enum.into(%{})
  end
  defp sanitize_for_json(data) when is_list(data) do
    Enum.map(data, &sanitize_for_json/1)
  end
  defp sanitize_for_json({key, value}) when is_atom(key) do
    %{"tuple_key" => Atom.to_string(key), "tuple_value" => sanitize_for_json(value)}
  end
  defp sanitize_for_json(value), do: value
end
