defmodule AprsmeWeb.MapLive.BoundsUpdater do
  @moduledoc """
  Handles bounds-change processing pipeline for the map LiveView.
  Extracted from Index to reduce module size.
  """

  import Phoenix.Component, only: [assign: 2]
  import Phoenix.LiveView, only: [push_event: 3]

  alias AprsmeWeb.Live.Shared.BoundsUtils
  alias AprsmeWeb.MapLive.DisplayManager
  alias AprsmeWeb.MapLive.HistoricalLoader
  alias AprsmeWeb.MapLive.Subscriptions
  alias Phoenix.LiveView.Socket

  require Logger

  @bounds_update_debounce_ms Application.compile_env(:aprsme, :bounds_update_debounce_ms, 400)

  @doc """
  Main entry point for bounds updates from client events.
  Delegates to debounce pipeline or processes immediately.
  """
  @spec handle_bounds_update(map(), Socket.t()) :: {:noreply, Socket.t()}
  def handle_bounds_update(bounds, socket) do
    if socket.assigns.historical_loading do
      {:noreply, assign(socket, pending_bounds: bounds)}
    else
      map_bounds = BoundsUtils.normalize_bounds(bounds)

      if valid_bounds?(map_bounds) do
        handle_valid_bounds_update(map_bounds, socket)
      else
        {:noreply, socket}
      end
    end
  end

  @doc """
  Process a pending bounds update from the debounce timer or from a forced initial load.
  """
  @spec process_bounds_update(map(), Socket.t()) :: Socket.t()
  def process_bounds_update(map_bounds, socket) do
    Logger.debug("process_bounds_update called with bounds: #{inspect(map_bounds)}")

    Subscriptions.update_viewport(socket, map_bounds)

    bounds_state = bounds_update_state(socket, map_bounds)
    log_bounds_update_state(bounds_state)

    {new_visible_packets, packets_to_remove} = filtered_packets_for_bounds(socket.assigns.visible_packets, map_bounds)

    socket =
      socket
      |> DisplayManager.remove_markers_batch(packets_to_remove)
      |> maybe_clear_historical_packets(bounds_state)
      |> push_event("filter_markers_by_bounds", %{bounds: map_bounds})
      |> assign(map_bounds: map_bounds, visible_packets: new_visible_packets)
      |> assign(needs_initial_historical_load: false)

    log_historical_reload(socket)

    socket = HistoricalLoader.start_progressive_historical_loading(socket)

    # If tracking a callsign and at low zoom, send trail line instead of letting
    # historical loader send heat map
    socket =
      if socket.assigns.tracked_callsign != "" and socket.assigns.map_zoom <= 8 do
        DisplayManager.send_trail_line_for_tracked_callsign(socket)
      else
        socket
      end

    # Mark initial historical as completed if this was the initial load
    if bounds_state.is_initial_load do
      assign(socket, initial_historical_completed: true)
    else
      socket
    end
  end

  @doc """
  Schedule a debounced bounds update. Cancels any pending timer first.
  """
  @spec schedule_bounds_update(map(), Socket.t()) :: {:noreply, Socket.t()}
  def schedule_bounds_update(map_bounds, socket) do
    if socket.assigns[:bounds_update_timer] do
      case Process.cancel_timer(socket.assigns.bounds_update_timer) do
        false ->
          receive do
            {:process_bounds_update, _} -> :ok
          after
            0 -> :ok
          end

        _ ->
          :ok
      end
    end

    socket = HistoricalLoader.cancel_pending_loads(socket)

    timer_ref = Process.send_after(self(), {:process_bounds_update, map_bounds}, @bounds_update_debounce_ms)
    {:noreply, assign(socket, bounds_update_timer: timer_ref, pending_bounds: map_bounds)}
  end

  # --- Private helpers ---

  defp valid_bounds?(map_bounds) do
    map_bounds.north <= 90 and map_bounds.south >= -90 and map_bounds.north > map_bounds.south
  end

  defp handle_valid_bounds_update(map_bounds, socket) do
    cond do
      socket.assigns[:needs_initial_historical_load] ->
        Logger.debug("Processing initial bounds update immediately (forced): #{inspect(map_bounds)}")
        socket = process_bounds_update(map_bounds, socket)
        {:noreply, socket}

      BoundsUtils.compare_bounds(map_bounds, socket.assigns.map_bounds) ->
        {:noreply, socket}

      true ->
        schedule_bounds_update(map_bounds, socket)
    end
  end

  defp bounds_update_state(socket, map_bounds) do
    %{
      is_initial_load: socket.assigns[:needs_initial_historical_load] || !socket.assigns[:initial_bounds_loaded],
      bounds_changed:
        socket.assigns.map_bounds && not BoundsUtils.compare_bounds(map_bounds, socket.assigns.map_bounds),
      initial_historical_completed: socket.assigns[:initial_historical_completed] || false
    }
  end

  defp log_bounds_update_state(bounds_state) do
    Logger.debug(
      "is_initial_load: #{bounds_state.is_initial_load}, bounds_changed: #{bounds_state.bounds_changed}, " <>
        "initial_historical_completed: #{bounds_state.initial_historical_completed}"
    )
  end

  defp filtered_packets_for_bounds(visible_packets, map_bounds) do
    {
      BoundsUtils.filter_packets_by_bounds(visible_packets, map_bounds),
      BoundsUtils.reject_packets_by_bounds(visible_packets, map_bounds)
    }
  end

  defp maybe_clear_historical_packets(socket, %{
         bounds_changed: true,
         is_initial_load: false,
         initial_historical_completed: true
       }) do
    Logger.debug("Bounds changed after initial load - clearing historical packets")
    push_event(socket, "clear_historical_packets", %{})
  end

  defp maybe_clear_historical_packets(socket, _bounds_state) do
    Logger.debug("Initial load or no significant change - keeping existing markers")
    socket
  end

  defp log_historical_reload(socket) do
    Logger.debug("Starting progressive historical loading for new bounds")

    Logger.debug(
      "Current assigns: historical_loading=#{socket.assigns.historical_loading}, map_bounds=#{inspect(socket.assigns.map_bounds)}"
    )
  end
end
