defmodule PushEx.Instrumentation.Tracker do
  @moduledoc false

  use GenServer

  @doc false
  def start_link(_) do
    GenServer.start_link(__MODULE__, [], name: __MODULE__)
  end

  @doc false
  def init(_) do
    Process.flag(:trap_exit, true)

    {:ok, %{channel_pids: %{}, transport_pids: %{}}}
  end

  def connected_socket_count(opts \\ []) do
    pid = Keyword.get(opts, :pid, __MODULE__)
    GenServer.call(pid, :connected_socket_count)
  end

  def connected_channel_count(opts \\ []) do
    pid = Keyword.get(opts, :pid, __MODULE__)
    GenServer.call(pid, :connected_channel_count)
  end

  def connected_transport_pids(opts \\ []) do
    pid = Keyword.get(opts, :pid, __MODULE__)
    GenServer.call(pid, :connected_transport_pids)
  end

  def connected_channel_pids(opts \\ []) do
    pid = Keyword.get(opts, :pid, __MODULE__)
    GenServer.call(pid, :connected_channel_pids)
  end

  @doc false
  def track_channel(socket = %Phoenix.Socket{}, opts \\ []) do
    pid = Keyword.get(opts, :pid, __MODULE__)
    GenServer.call(pid, {:track, socket})
  end

  @doc false
  def track_socket(socket = %Phoenix.Socket{}, opts \\ []) do
    pid = Keyword.get(opts, :pid, __MODULE__)
    GenServer.call(pid, {:track_socket, socket})
  end

  @doc false
  def state(opts \\ []) do
    pid = Keyword.get(opts, :pid, __MODULE__)
    GenServer.call(pid, :state)
  end

  ## Callbacks

  def handle_call(:connected_channel_count, _from, state = %{channel_pids: pids}) do
    {:reply, map_size(pids), state}
  end

  def handle_call(:connected_socket_count, _from, state = %{transport_pids: pids}) do
    {:reply, map_size(pids), state}
  end

  def handle_call(:connected_transport_pids, _from, state = %{transport_pids: pids}) do
    {:reply, Map.keys(pids), state}
  end

  def handle_call(:connected_channel_pids, _from, state = %{channel_pids: pids}) do
    {:reply, Map.keys(pids), state}
  end

  def handle_call(
        {:track, socket = %Phoenix.Socket{channel_pid: pid, topic: topic}},
        _from,
        state = %{channel_pids: pids}
      ) do
    link_processes_to_capture_bidirectional_exits(pid)

    id = PushEx.Config.socket_impl().presence_identifier(socket)
    online_at = PushEx.unix_ms_now()

    new_channel_pids =
      Map.put(pids, pid, %{
        channel: topic,
        identifier: id,
        online_at: online_at
      })

    {:reply, :ok, %{state | channel_pids: new_channel_pids}}
  end

  def handle_call(
        {:track_socket, socket = %Phoenix.Socket{transport: transport, transport_pid: transport_pid}},
        _from,
        state = %{transport_pids: transport_pids}
      ) do
    link_processes_to_capture_bidirectional_exits(transport_pid)

    id = PushEx.Config.socket_impl().presence_identifier(socket)
    online_at = PushEx.unix_ms_now()

    new_transport_pids =
      Map.put(transport_pids, transport_pid, %{
        type: transport,
        identifier: id,
        online_at: online_at
      })

    {:reply, :ok, %{state | transport_pids: new_transport_pids}}
  end

  def handle_call(:state, _from, state) do
    {:reply, state, state}
  end

  def handle_info({:EXIT, pid, _reason}, state = %{channel_pids: pids, transport_pids: transport_pids}) do
    new_channel_pids = Map.delete(pids, pid)
    new_transport_pids = Map.delete(transport_pids, pid)

    {:noreply, %{state | channel_pids: new_channel_pids, transport_pids: new_transport_pids}}
  end

  defp link_processes_to_capture_bidirectional_exits(pid), do: Process.link(pid)
end
