defmodule Forum.Processes do
  @moduledoc """
  Snapshot of every BEAM process for the /processes endpoint. Pure data
  function — the router serves a shell HTML and the WS handler returns
  the rows produced here.
  """

  @doc """
  Returns a list of maps, one per live process, sorted by memory desc.

  Each map has string keys (JSON-friendly):
    "pid"          — inspected PID string, e.g. "#PID<0.123.0>"
    "parent"       — inspected PID string of immediate parent (from
                     $ancestors), or nil for roots / non-proc_lib processes
    "name"         — registered name string, or nil
    "initial_call" — real $initial_call MFA, formatted as "Mod.fun/arity"
    "memory_kb"    — process memory in KB
    "tree_memory_kb" — process memory plus all descendant process memory in KB
    "msgs"         — message queue length
    "status"       — :running | :runnable | :waiting | :suspended | :exiting
    "mine"         — true if the process is under Forum.Supervisor's tree
  """
  # Registries to consult for "via" names — processes registered as
  # {:via, Registry, {Foo, key}} don't show up under :registered_name,
  # so we look them up by pid here and use the key as the display name.
  @registries [
    Forum.ProcessRegistry,
    Forum.FormsSubscriberRegistry
  ]

  def list do
    my_pids = my_pids()

    Process.list()
    |> Enum.map(&info_for(&1, my_pids))
    |> Enum.reject(&is_nil/1)
    |> add_tree_memory()
    |> Enum.sort_by(& &1["memory_kb"], :desc)
  end

  defp add_tree_memory(rows) do
    by_parent = Enum.group_by(rows, & &1["parent"])
    {totals, _memo} =
      Enum.map_reduce(rows, %{}, fn row, memo ->
        {total, memo} = tree_memory(row, by_parent, memo)
        {{row["pid"], total}, memo}
      end)

    totals = Map.new(totals)

    Enum.map(rows, fn row ->
      Map.put(row, "tree_memory_kb", Map.fetch!(totals, row["pid"]))
    end)
  end

  defp tree_memory(row, by_parent, memo) do
    pid = row["pid"]

    case memo do
      %{^pid => total} ->
        {total, memo}

      %{} ->
        children = Map.get(by_parent, pid, [])

        {children_total, memo} =
          Enum.reduce(children, {0, memo}, fn child, {sum, memo} ->
            {total, memo} = tree_memory(child, by_parent, memo)
            {sum + total, memo}
          end)

        total = row["memory_kb"] + children_total
        {total, Map.put(memo, pid, total)}
    end
  end

  defp info_for(pid, my_pids) do
    case Process.info(pid, [
           :registered_name,
           :initial_call,
           :memory,
           :message_queue_len,
           :status,
           :dictionary
         ]) do
      nil ->
        nil

      info ->
        dict = info[:dictionary] || []
        real_call = Keyword.get(dict, :"$initial_call") || info[:initial_call]
        ancestors = Keyword.get(dict, :"$ancestors", [])

        name =
          format_name(info[:registered_name]) || lookup_registry_name(pid)

        %{
          "pid" => inspect(pid),
          "parent" => parent_string(ancestors),
          "name" => name,
          "initial_call" => format_mfa(real_call),
          "memory_kb" => div(info[:memory] || 0, 1024),
          "msgs" => info[:message_queue_len] || 0,
          "status" => info[:status],
          "mine" => MapSet.member?(my_pids, pid)
        }
    end
  end

  defp lookup_registry_name(pid) do
    @registries
    |> Enum.flat_map(fn registry ->
      try do
        Registry.keys(registry, pid) |> Enum.map(&format_registry_key/1)
      rescue
        # Registry not started yet (boot race) — skip silently.
        ArgumentError -> []
      end
    end)
    |> case do
      [] -> nil
      names -> Enum.join(names, ", ")
    end
  end

  # {"Task", "1"} -> "Task#1". Falls back to inspect for unknown shapes.
  defp format_registry_key({:form, {class, id}}) when is_binary(class), do: "#{class}##{id}"
  defp format_registry_key({:network, slug}), do: "network:#{slug}"
  defp format_registry_key({:public_site, network}), do: "public-site:#{network}"
  defp format_registry_key({:http_server, :main}), do: "http-server:main"
  defp format_registry_key({:http_server, {:network, network}}), do: "http-server:#{network}"
  defp format_registry_key(:forms), do: "forms-subscriber:forms"
  defp format_registry_key(other), do: inspect(other)

  # The first entry in $ancestors is the immediate parent. proc_lib
  # stores PIDs directly; for processes registered by name it can also
  # be the registered name (atom), which we resolve to its current PID.
  defp parent_string([pid | _]) when is_pid(pid), do: inspect(pid)

  defp parent_string([name | _]) when is_atom(name) do
    case Process.whereis(name) do
      nil -> nil
      pid -> inspect(pid)
    end
  end

  defp parent_string(_), do: nil

  defp my_pids do
    case Process.whereis(Forum.Supervisor) do
      nil -> MapSet.new()
      sup -> MapSet.new([sup | all_descendants(sup)])
    end
  end

  defp all_descendants(sup) do
    Supervisor.which_children(sup)
    |> Enum.flat_map(fn {_id, pid, type, _modules} ->
      cond do
        not is_pid(pid) -> []
        type == :supervisor -> [pid | all_descendants(pid)]
        true -> [pid]
      end
    end)
  end

  defp format_name([]), do: nil
  defp format_name(name) when is_atom(name), do: inspect(name)
  defp format_name(_), do: nil

  defp format_mfa({m, f, a}) when is_atom(m) and is_atom(f) and is_integer(a),
    do: "#{inspect(m)}.#{f}/#{a}"

  defp format_mfa(_), do: "-"
end
