defmodule AprsmeWeb.PacketsLive.Index do
  @moduledoc false
  use AprsmeWeb, :live_view
  use Gettext, backend: AprsmeWeb.Gettext

  alias Aprsme.EncodingUtils

  require Logger

  @flush_interval_ms 150
  @max_stream_size 100

  @impl true
  def mount(_params, _session, socket) do
    if connected?(socket) do
      case Phoenix.PubSub.subscribe(Aprsme.PubSub, "postgres:aprsme_packets") do
        :ok ->
          :ok

        {:error, reason} ->
          Logger.error("Failed to subscribe to postgres:aprsme_packets: #{inspect(reason)}")
      end
    end

    {:ok,
     socket
     |> put_private(:packet_buffer, [])
     |> put_private(:flush_timer, nil)
     |> stream(:packets, [], at: 0)}
  end

  @impl true
  def handle_info({:postgres_packet, payload}, socket) do
    # Buffer in socket.private to avoid triggering a rerender on every packet.
    # A periodic flush timer drains the buffer and batch-inserts into the stream,
    # which keeps client-side morphdom from being overwhelmed during high-traffic bursts.
    socket = update_in(socket.private.packet_buffer, &[payload | &1])
    socket = ensure_flush_timer(socket)
    {:noreply, socket}
  end

  @impl true
  def handle_info(:flush_packets, socket) do
    packets = Enum.reverse(socket.private.packet_buffer)
    socket = put_private(socket, :packet_buffer, [])
    socket = put_private(socket, :flush_timer, nil)

    # Batch insert all accumulated packets into the stream.
    # Processing newest-first with at: 0 preserves descending-chronological order.
    socket =
      Enum.reduce(packets, socket, fn payload, s ->
        sanitized = EncodingUtils.sanitize_packet(payload)
        stream_insert(s, :packets, sanitized, at: 0)
      end)

    # Trim stream to keep at most @max_stream_size items.
    socket = trim_stream(socket)

    {:noreply, socket}
  end

  defp ensure_flush_timer(socket) do
    if socket.private.flush_timer do
      socket
    else
      timer = Process.send_after(self(), :flush_packets, @flush_interval_ms)
      put_private(socket, :flush_timer, timer)
    end
  end

  defp trim_stream(socket) do
    stream = socket.assigns.streams.packets

    stream
    |> Enum.to_list()
    |> Enum.reverse()
    |> do_trim(socket)
  end

  defp do_trim(packets, socket) when length(packets) <= @max_stream_size, do: socket

  defp do_trim(packets, socket) do
    {_kept, to_remove} = Enum.split(packets, @max_stream_size)

    Enum.reduce(to_remove, socket, fn {id, _}, s ->
      stream_delete(s, :packets, id)
    end)
  end

  @doc """
  Extract a coordinate (:lat or :lon) from a packet, checking multiple sources:
  1. Direct :lat/:lon keys
  2. Packet struct with PostGIS location
  3. data_extended map with :latitude/:longitude keys
  """
  def extract_coordinate(packet, which) when which in [:lat, :lon] do
    direct_key = which
    extended_key = if which == :lat, do: :latitude, else: :longitude

    Map.get(packet, direct_key) ||
      extract_from_location(packet, which) ||
      extract_from_data_extended(packet, extended_key)
  end

  @doc """
  Format a coordinate value for display with up to 6 decimal places.
  """
  def format_coordinate(nil), do: ""

  def format_coordinate(value) when is_float(value) do
    "~.6f" |> :io_lib.format([value]) |> List.to_string()
  end

  def format_coordinate(value) when is_binary(value) do
    Regex.replace(~r/(\d+\.\d{1,6})\d*/, value, "\\1")
  end

  def format_coordinate(value), do: to_string(value)

  defp extract_from_location(%Aprsme.Packet{location: %Geo.Point{coordinates: {_lon, lat}}}, :lat), do: lat
  defp extract_from_location(%Aprsme.Packet{location: %Geo.Point{coordinates: {lon, _lat}}}, :lon), do: lon
  defp extract_from_location(_, _), do: nil

  defp extract_from_data_extended(packet, key) do
    case Map.get(packet, :data_extended) do
      %{} = data -> Map.get(data, key) || Map.get(data, to_string(key))
      _ -> nil
    end
  end
end
