defmodule AprsmeWeb.MapLive.DisplayManager do
  @moduledoc """
  Handles heat maps, markers, and zoom threshold display logic.
  """

  alias Aprsme.Packets.Clustering
  alias AprsmeWeb.Live.Shared.BoundsUtils
  alias AprsmeWeb.MapLive.DataBuilder
  alias Phoenix.LiveView
  alias Phoenix.LiveView.Socket

  # Zoom level at or below which the map switches to heat map mode
  @heat_map_max_zoom 8

  @doc """
  Handle zoom threshold crossing between heat map and marker modes.
  When tracking a callsign at low zoom, show trail line instead of heat map.
  """
  @spec handle_zoom_threshold_crossing(Socket.t(), integer()) :: Socket.t()
  def handle_zoom_threshold_crossing(socket, zoom) do
    tracked_callsign = socket.assigns.tracked_callsign || ""

    if zoom <= @heat_map_max_zoom do
      # Switching to low zoom mode
      socket = LiveView.push_event(socket, "clear_all_markers", %{})

      # If tracking a callsign, show trail line; otherwise show heat map
      if tracked_callsign == "" do
        send_heat_map_for_current_bounds(socket)
      else
        send_trail_line_for_tracked_callsign(socket)
      end
    else
      # Switching to markers
      trigger_marker_display(socket)
    end
  end

  @doc """
  Check if zoom level crosses the threshold between heat map and marker modes.
  """
  @spec crossing_zoom_threshold?(integer(), integer()) :: boolean()
  def crossing_zoom_threshold?(old_zoom, new_zoom) do
    (old_zoom <= @heat_map_max_zoom and new_zoom > @heat_map_max_zoom) or
      (old_zoom > @heat_map_max_zoom and new_zoom <= @heat_map_max_zoom)
  end

  @doc """
  Send heat map data for current bounds.
  """
  @spec send_heat_map_for_current_bounds(Socket.t()) :: Socket.t()
  def send_heat_map_for_current_bounds(socket) do
    # Get all packets within current bounds
    all_packets =
      Map.values(socket.assigns.visible_packets) ++
        Map.values(socket.assigns.historical_packets)

    # Filter by bounds
    filtered_packets =
      all_packets
      |> BoundsUtils.filter_packets_by_bounds(socket.assigns.map_bounds)
      |> Enum.uniq_by(fn packet ->
        Map.get(packet, :id) || Map.get(packet, "id")
      end)

    send_heat_map_for_packets(socket, filtered_packets)
  end

  @doc """
  Send heat map data for specific packets.
  """
  @spec send_heat_map_for_packets(Socket.t(), list()) :: Socket.t()
  def send_heat_map_for_packets(socket, packets) do
    # Get clustering data
    case Clustering.cluster_packets(packets, socket.assigns.map_zoom) do
      {:heat_map, heat_points} ->
        LiveView.push_event(socket, "show_heat_map", %{heat_points: heat_points})

      {:raw_packets, _packets} ->
        # Shouldn't happen at zoom <= 8, but handle it anyway
        socket
    end
  end

  @doc """
  Send trail line data for tracked callsign.
  Connects all position points chronologically, regardless of distance.
  """
  @spec send_trail_line_for_tracked_callsign(Socket.t()) :: Socket.t()
  def send_trail_line_for_tracked_callsign(socket) do
    callsign = socket.assigns.tracked_callsign
    threshold = socket.assigns.packet_age_threshold

    packets =
      trail_packets_for_callsign(
        socket.assigns.visible_packets,
        socket.assigns.historical_packets,
        callsign,
        threshold
      )

    if packets == [] do
      socket
    else
      push_trail_line(socket, callsign, packets)
    end
  end

  @doc """
  Send heat map data for filtered packets.
  """
  @spec send_heat_map_data(Socket.t(), map()) :: Socket.t()
  def send_heat_map_data(socket, filtered_packets) do
    # Convert map of packets to list
    packet_list = Map.values(filtered_packets)
    send_heat_map_for_packets(socket, packet_list)
  end

  @doc """
  Trigger marker display mode.
  """
  @spec trigger_marker_display(Socket.t()) :: Socket.t()
  def trigger_marker_display(socket) do
    # Clear heat map and show markers
    socket = LiveView.push_event(socket, "show_markers", %{})

    # Re-send all visible packets as markers
    visible_packets_list = DataBuilder.build_packet_data_list_from_map(socket.assigns.visible_packets, true, socket)

    socket = add_markers_if_any(socket, visible_packets_list)

    # Trigger historical packet reload for markers
    start_progressive_historical_loading(socket)
  end

  @doc """
  Add markers to map if any exist.
  """
  @spec add_markers_if_any(Socket.t(), list()) :: Socket.t()
  def add_markers_if_any(socket, []), do: socket

  def add_markers_if_any(socket, markers) do
    LiveView.push_event(socket, "add_markers", %{markers: markers})
  end

  @doc """
  Remove multiple markers from map.
  """
  @spec remove_markers_batch(Socket.t(), list()) :: Socket.t()
  def remove_markers_batch(socket, []), do: socket

  def remove_markers_batch(socket, marker_ids) do
    LiveView.push_event(socket, "remove_markers_batch", %{ids: marker_ids})
  end

  # Placeholder for historical loading function - this should be moved to HistoricalLoader
  defp start_progressive_historical_loading(socket) do
    # This function should be moved to HistoricalLoader module
    socket
  end

  defp trail_packets_for_callsign(visible_packets, historical_packets, callsign, threshold) do
    (Map.values(visible_packets) ++ Map.values(historical_packets))
    |> Enum.reduce(%{}, fn p, acc ->
      Map.put_new(acc, trail_packet_key(p), p)
    end)
    |> Map.values()
    |> Enum.filter(&tracked_packet?(&1, callsign, threshold))
    |> Enum.sort_by(&packet_timestamp/1, :asc)
  end

  defp tracked_packet?(packet, callsign, threshold) do
    sender = Map.get(packet, :sender) || Map.get(packet, "sender")
    received_at = packet_received_at(packet)

    String.upcase(sender) == String.upcase(callsign) &&
      DateTime.compare(received_at, threshold) != :lt
  end

  defp push_trail_line(socket, callsign, packets) do
    LiveView.push_event(socket, "show_trail_line", %{
      callsign: callsign,
      points: Enum.map(packets, &trail_point/1)
    })
  end

  defp trail_point(packet) do
    lat = Map.get(packet, :lat) || Map.get(packet, "lat")
    lon = Map.get(packet, :lon) || Map.get(packet, "lon")
    received_at = packet_received_at(packet)

    %{
      lat: Aprsme.EncodingUtils.to_float(lat) || 0.0,
      lng: Aprsme.EncodingUtils.to_float(lon) || 0.0,
      timestamp: DateTime.to_iso8601(received_at)
    }
  end

  defp packet_received_at(packet) do
    Map.get(packet, :received_at) || Map.get(packet, "received_at")
  end

  defp packet_timestamp(packet) do
    packet
    |> packet_received_at()
    |> DateTime.to_unix(:microsecond)
  end

  defp trail_packet_key(packet) do
    Map.get(packet, :id) ||
      Map.get(packet, "id") ||
      {
        Map.get(packet, :sender) || Map.get(packet, "sender"),
        Map.get(packet, :lat) || Map.get(packet, "lat"),
        Map.get(packet, :lon) || Map.get(packet, "lon"),
        packet_received_at(packet)
      }
  end
end
