defmodule AprsmeWeb.MapLive.Subscriptions do
  @moduledoc """
  Manages Phoenix.PubSub subscriptions and connection lifecycle for the map LiveView.
  Extracted from Index to reduce module size.
  """

  import Phoenix.Component, only: [assign: 3]
  import Phoenix.LiveView, only: [connected?: 1, push_event: 3, put_flash: 3]

  alias Phoenix.LiveView.Socket

  @doc """
  Set up spatial and PubSub subscriptions for a newly mounted socket.
  Only creates subscriptions when the socket is connected (LiveSocket).
  """
  @spec setup(Socket.t(), map()) :: Socket.t()
  def setup(socket, initial_bounds) do
    do_setup(socket, initial_bounds, connected?(socket))
  end

  @doc """
  Clean up all subscriptions and timers when the LiveView terminates.
  """
  @spec teardown(Socket.t()) :: :ok
  def teardown(socket) do
    _teardown_connection_monitor(socket)
    _teardown_spatial(socket)
    _teardown_all_timers(socket)
    _teardown_batch_tasks(socket)
    :ok
  end

  @doc """
  Update the spatial viewport bounds for an existing subscription.
  """
  @spec update_viewport(Socket.t(), map()) :: :ok | nil
  def update_viewport(socket, map_bounds) do
    if socket.assigns[:spatial_client_id] do
      Aprsme.SpatialPubSub.update_viewport(socket.assigns.spatial_client_id, map_bounds)
    end
  end

  # --- Private helpers ---

  defp do_setup(socket, _initial_bounds, false), do: socket

  defp do_setup(socket, initial_bounds, true) do
    # Check if we should accept new connections
    if Application.get_env(:aprsme, :cluster_enabled, false) and
         not Aprsme.ConnectionMonitor.accepting_connections?() do
      socket
      |> put_flash(:info, "This server is currently at capacity. Please try again in a moment.")
      |> push_event("redirect_to_least_loaded", %{})
      |> assign(:connection_draining, true)
    else
      # Generate a unique client ID for this LiveView instance
      client_id = "liveview_#{:erlang.phash2(self())}"

      # Register with connection monitor
      _ =
        if Application.get_env(:aprsme, :cluster_enabled, false) do
          _ = Aprsme.ConnectionMonitor.register_connection()
          # Subscribe to drain events for this node
          Phoenix.PubSub.subscribe(Aprsme.PubSub, "connection:drain:#{Node.self()}")
        end

      # Register the actual initial viewport.
      {:ok, spatial_topic} = Aprsme.SpatialPubSub.register_viewport(client_id, initial_bounds)

      # Subscribe to the spatial topic for this client
      :ok = Phoenix.PubSub.subscribe(Aprsme.PubSub, spatial_topic)

      # Still subscribe to bad packets (they don't have location)
      :ok = Phoenix.PubSub.subscribe(Aprsme.PubSub, "bad_packets")

      # Subscribe to deployment events
      :ok = Phoenix.PubSub.subscribe(Aprsme.PubSub, "deployment_events")

      _ = Process.send_after(self(), :cleanup_old_packets, 60_000)

      socket
      |> assign(:spatial_client_id, client_id)
      |> assign(:connection_draining, false)
    end
  end

  defp _teardown_connection_monitor(socket) do
    _ =
      if Application.get_env(:aprsme, :cluster_enabled, false) and
           not socket.assigns[:connection_draining] do
        Aprsme.ConnectionMonitor.unregister_connection()
      end
  end

  defp _teardown_spatial(socket) do
    _ =
      if socket.assigns[:spatial_client_id] do
        Aprsme.SpatialPubSub.unregister_client(socket.assigns.spatial_client_id)
      end
  end

  defp _teardown_all_timers(socket) do
    cancel_if_exists(socket.assigns[:buffer_timer])
    cancel_if_exists(socket.assigns[:bounds_update_timer])
    cancel_if_exists(socket.assigns[:hover_end_timer])
  end

  defp cancel_if_exists(nil), do: :ok
  defp cancel_if_exists(timer_ref), do: Process.cancel_timer(timer_ref)

  defp _teardown_batch_tasks(socket) do
    _ =
      if socket.assigns[:pending_batch_tasks] do
        Enum.each(socket.assigns.pending_batch_tasks, &Process.cancel_timer/1)
      end
  end
end
