defmodule AprsmeWeb.PacketsLive.CallsignView do
  @moduledoc false
  use AprsmeWeb, :live_view

  import Ecto.Query

  alias Aprsme.Callsign
  alias Aprsme.DeviceParser
  alias Aprsme.EncodingUtils
  alias Aprsme.Packet
  alias Aprsme.Repo
  alias AprsmeWeb.Live.SharedPacketHandler

  @flush_interval_ms 150

  @impl true
  def mount(%{"callsign" => callsign}, _session, socket) do
    normalized = Callsign.normalize(callsign)

    if Callsign.valid?(normalized) do
      socket = subscribe_if_connected(socket, normalized)
      stored = get_stored_packets(normalized, 100)
      {:ok, assign_valid(socket, normalized, stored)}
    else
      {:ok, assign_invalid(socket, normalized)}
    end
  end

  defp subscribe_if_connected(socket, callsign) do
    _ =
      if connected?(socket) do
        Phoenix.PubSub.subscribe(Aprsme.PubSub, "packets:#{callsign}")
      end

    socket
  end

  defp assign_valid(socket, callsign, stored) do
    socket
    |> assign(:callsign, callsign)
    |> assign(:packets, stored)
    |> assign(:live_packets, [])
    |> assign(:all_packets, stored)
    |> assign(:error, nil)
    |> put_private(:packet_buffer, [])
    |> put_private(:flush_timer, nil)
  end

  defp assign_invalid(socket, callsign) do
    socket
    |> assign(:callsign, callsign)
    |> assign(:packets, [])
    |> assign(:live_packets, [])
    |> assign(:all_packets, [])
    |> assign(:error, "Invalid callsign format")
  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 updates assigns in one batch,
    # 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)

    {updated_stored, updated_live} =
      packets
      # Sanitize encoding first, then batch enrich with device info
      # (enrich_packets_with_device_info batches device lookups internally)
      |> Enum.map(&EncodingUtils.sanitize_packet/1)
      |> SharedPacketHandler.enrich_packets_with_device_info()
      # Add canonical device identifier per-packet
      |> Enum.map(&enrich_packet_with_device_identifier/1)
      # Accumulate through the packet list update logic
      |> Enum.reduce({socket.assigns.packets, socket.assigns.live_packets}, fn enriched, {stored, live} ->
        update_packet_lists(stored, live, enriched)
      end)

    all_packets = get_all_packets_list(updated_stored, updated_live)

    {:noreply,
     assign(socket,
       packets: updated_stored,
       live_packets: updated_live,
       all_packets: all_packets
     )}
  end

  def handle_info(_message, socket), do: {:noreply, socket}

  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 enrich_packet_with_device_identifier(sanitized_payload) do
    device_identifier =
      Map.get(sanitized_payload, :device_identifier) ||
        Map.get(sanitized_payload, "device_identifier") ||
        DeviceParser.extract_device_identifier(sanitized_payload)

    canonical_identifier = resolve_canonical_identifier(device_identifier)
    Map.put(sanitized_payload, :device_identifier, canonical_identifier)
  end

  defp resolve_canonical_identifier(identifier) when is_binary(identifier) do
    case Aprsme.DeviceIdentification.lookup_device_by_identifier(identifier) do
      %{identifier: canonical} -> canonical
      nil -> identifier
    end
  end

  defp resolve_canonical_identifier(identifier), do: identifier

  # Get recent packets for this callsign from the database (all packets, not just position)
  # Returns the latest packets regardless of age, filtered by callsign
  defp get_stored_packets(callsign, limit) do
    # Create query for all packets (not just position packets) for this callsign
    query =
      from p in Packet,
        order_by: [desc: p.received_at],
        limit: ^limit

    # Apply callsign filter using sender field for exact matching
    # Use functional index on upper(sender) for performance
    normalized_callsign = String.upcase(String.trim(callsign))
    filtered_query = from p in query, where: fragment("upper(?)", p.sender) == ^normalized_callsign

    filtered_query
    |> Repo.all()
    |> Enum.map(&EncodingUtils.sanitize_packet/1)
    |> SharedPacketHandler.enrich_packets_with_device_info()
  rescue
    error ->
      require Logger

      Logger.error("Failed to fetch stored packets for callsign #{callsign}: #{inspect(error)}")
      []
  end

  # Helper to get all packets (stored + live) in chronological order
  # Combines stored and live packets, sorts by timestamp (newest first), and limits to 100
  defp get_all_packets_list(stored, live) do
    # Combine and sort by received_at timestamp (newest first)
    (live ++ stored)
    |> Enum.sort_by(&get_timestamp_microseconds/1, :desc)
    # Ensure we never exceed 100 total
    |> Enum.take(100)
  end

  defp get_timestamp_microseconds(%DateTime{} = dt), do: DateTime.to_unix(dt, :microsecond)

  defp get_timestamp_microseconds(dt) when is_binary(dt) do
    case DateTime.from_iso8601(dt) do
      {:ok, parsed_dt, _} -> DateTime.to_unix(parsed_dt, :microsecond)
      _ -> 0
    end
  end

  defp get_timestamp_microseconds(_), do: 0

  # Helper to update stored and live packet lists, keeping total <= 100.
  # Dispatches on capacity: at-cap with no stored, at-cap with stored, under-cap.
  defp update_packet_lists(current_stored, current_live, sanitized_payload) do
    total = length(current_live) + length(current_stored)
    do_update(total, current_stored, current_live, sanitized_payload)
  end

  defp do_update(total, [], current_live, payload) when total >= 100, do: {[], [payload | Enum.drop(current_live, -1)]}

  defp do_update(total, current_stored, current_live, payload) when total >= 100,
    do: {Enum.drop(current_stored, -1), [payload | current_live]}

  defp do_update(_total, current_stored, current_live, payload), do: {current_stored, [payload | current_live]}
end
