defmodule RefugeWeb.InvocationsLive do
  @moduledoc """
  Shows all Bazel invocations / builds.
  Provides an overview of all build events happening in the system.
  """

  use RefugeWeb, :live_view
  require Logger

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

    {:ok,
     assign(socket,
       page_title: "All Invocations",
       invocations: %{},
       latest_events: []
     )}
  end

  @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

  @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
    # Extract event data
    stream_id = get_in(event, ["stream_id"]) || %{}
    build_id = Map.get(stream_id, "build_id")
    invocation_id = Map.get(stream_id, "invocation_id", "")
    _event_type = Map.get(event, "event_type", "unknown")  # Prefix with underscore to avoid unused variable warning
    
    # Skip events without a build_id
    if is_nil(build_id) do
      {:noreply, socket}
    else
      # Process the event based on type
      invocations = update_invocation(socket.assigns.invocations, build_id, invocation_id, event)
      
      # Keep the last 20 events for the activity feed
      latest_events = 
        [format_latest_event(build_id, invocation_id, event) | socket.assigns.latest_events]
        |> Enum.take(20)
      
      {:noreply, assign(socket, invocations: invocations, latest_events: latest_events)}
    end
  end

  defp update_invocation(invocations, build_id, invocation_id, event) do
    event_type = Map.get(event, "event_type", "unknown")
    event_data = Map.get(event, "event", %{})
    
    invocation = Map.get(invocations, build_id, %{
      "build_id" => build_id,
      "invocation_ids" => MapSet.new(),
      "status" => "in_progress",
      "start_time" => DateTime.utc_now() |> DateTime.to_string(),
      "end_time" => nil,
      "event_count" => 0
    })
    
    # Update invocation with this event's data
    updated =
      invocation
      |> Map.update("event_count", 1, &(&1 + 1))
      |> update_invocation_ids(invocation_id)
      |> update_status_from_event(event_type, event_data)
    
    Map.put(invocations, build_id, updated)
  end
  
  defp update_invocation_ids(invocation, ""), do: invocation
  defp update_invocation_ids(invocation, invocation_id) do
    updated_ids = 
      invocation
      |> Map.get("invocation_ids", MapSet.new())
      |> MapSet.put(invocation_id)
      
    Map.put(invocation, "invocation_ids", updated_ids)
  end
  
  defp update_status_from_event(invocation, "lifecycle", event_data) do
    # Check for build finished events
    case event_data do
      %{"build_finished" => %{"status" => %{"result" => result}}} ->
        invocation
        |> Map.put("status", sanitize_status(result))
        |> Map.put("end_time", DateTime.utc_now() |> DateTime.to_string())
        
      %{"invocation_attempt_finished" => %{"invocation_status" => %{"result" => result}}} ->
        invocation
        |> Map.put("status", sanitize_status(result))
        |> Map.put("end_time", DateTime.utc_now() |> DateTime.to_string())
        
      _ ->
        invocation
    end
  end
  
  defp update_status_from_event(invocation, _event_type, _event_data), do: invocation
  
  defp sanitize_status(status) when is_atom(status), do: status |> Atom.to_string() |> String.downcase()
  defp sanitize_status(status) when is_binary(status), do: String.downcase(status)
  defp sanitize_status(_), do: "unknown"
  
  defp format_latest_event(build_id, invocation_id, event) do
    event_type = Map.get(event, "event_type", "unknown")
    timestamp = event["timestamp"] || DateTime.utc_now() |> DateTime.to_string()
    
    # Create a sanitized version of the event for display
    event_data = 
      event
      |> Map.get("event", %{})
      |> sanitize_for_json()
    
    %{
      "build_id" => build_id,
      "invocation_id" => invocation_id,
      "event_type" => event_type,
      "timestamp" => timestamp,
      "event_data" => event_data
    }
  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
  # Special case for structs (including Google Protobuf structs)
  defp sanitize_for_json(%{__struct__: _} = data) do
    # Convert the struct to a map and then sanitize it
    data
    |> Map.from_struct()
    |> Map.put("_struct_type", data.__struct__ |> to_string())
    |> sanitize_for_json()
  end
  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
  # Handle tuples that aren't key-value pairs
  defp sanitize_for_json(data) when is_tuple(data) do
    data
    |> Tuple.to_list()
    |> Enum.with_index()
    |> Enum.map(fn {value, index} -> {"#{index}", sanitize_for_json(value)} end)
    |> Enum.into(%{})
    |> Map.put("_tuple", true)
  end
  defp sanitize_for_json(value), do: value
end
