defmodule AprsmeWeb.Live.Shared.PacketUtils do
  @moduledoc """
  Shared packet processing and memory management utilities.
  Used across multiple LiveView modules for consistent packet handling.
  """

  alias AprsmeWeb.Live.Shared.BoundsUtils

  @doc """
  Get unique callsign key from packet.
  Returns the display name, or "__unknown__" if none is available.
  """
  @spec get_callsign_key(map()) :: binary()
  def get_callsign_key(packet) when is_map(packet) do
    case display_name(packet) do
      "" -> "__unknown__"
      callsign -> callsign
    end
  end

  def get_callsign_key(_packet), do: "__unknown__"

  @doc """
  Prune oldest packets from a map to enforce memory limits.
  """
  @spec prune_oldest_packets(map(), integer()) :: map()
  def prune_oldest_packets(packets_map, max_count) when map_size(packets_map) <= max_count do
    packets_map
  end

  def prune_oldest_packets(packets_map, max_count) do
    # Convert to list, sort by timestamp, take newest max_count packets
    packets_map
    |> Enum.to_list()
    |> Enum.sort_by(
      fn {_key, packet} ->
        case packet do
          %{received_at: %DateTime{} = dt} -> DateTime.to_unix(dt)
          %{"received_at" => timestamp} when is_integer(timestamp) -> timestamp
          _ -> 0
        end
      end,
      :desc
    )
    |> Enum.take(max_count)
    |> Map.new()
  end

  @doc """
  Parse trail duration with validation and bounds checking.
  """
  @spec parse_trail_duration(any()) :: 1 | 6 | 12 | 24 | 48 | 168
  def parse_trail_duration(duration) when is_binary(duration) do
    case Integer.parse(duration) do
      {hours, ""} when hours in [1, 6, 12, 24, 48, 168] ->
        hours

      _ ->
        # Default to 1 hour if invalid
        1
    end
  end

  def parse_trail_duration(_), do: 1

  @doc """
  Parse historical hours with validation.
  """
  @spec parse_historical_hours(any()) :: 1 | 3 | 6 | 12 | 24
  def parse_historical_hours(hours) when is_binary(hours) do
    case Integer.parse(hours) do
      {h, ""} when h in [1, 3, 6, 12, 24] ->
        h

      _ ->
        # Default to 1 hour if invalid
        1
    end
  end

  def parse_historical_hours(_), do: 1

  @doc """
  Filter packets by both time threshold and bounds.
  """
  @spec filter_packets_by_time_and_bounds(map(), map(), DateTime.t()) :: map()
  def filter_packets_by_time_and_bounds(packets, bounds, time_threshold) do
    packets
    |> Enum.filter(fn {_callsign, packet} ->
      BoundsUtils.within_bounds?(packet, bounds) &&
        packet_within_time_threshold?(packet, time_threshold)
    end)
    |> Map.new()
  end

  @doc """
  Check if packet contains weather data.
  Uses the pre-computed has_weather boolean when available (DB structs),
  falls back to field-by-field scan for live PubSub payloads.
  """
  @spec has_weather_data?(map()) :: boolean()
  def has_weather_data?(packet) do
    case Map.get(packet, :has_weather) || Map.get(packet, "has_weather") do
      nil ->
        Enum.any?(Aprsme.EncodingUtils.weather_fields(), fn field ->
          value = get_packet_field(packet, field, nil)
          not is_nil(value)
        end)

      bool when is_boolean(bool) ->
        bool
    end
  end

  @doc """
  Get weather field value from packet with fallback.
  """
  @spec get_weather_field(map(), atom()) :: any()
  def get_weather_field(packet, field) do
    data_extended = Map.get(packet, :data_extended, Map.get(packet, "data_extended", %{})) || %{}

    Map.get(packet, field) ||
      Map.get(packet, to_string(field)) ||
      Map.get(data_extended, field) ||
      Map.get(data_extended, to_string(field))
  end

  @doc """
  Get packet field value with fallback and default.
  """
  @spec get_packet_field(map(), atom() | String.t(), any()) :: any()
  def get_packet_field(packet, field, default \\ nil) do
    case Map.get(packet, field) do
      nil ->
        case Map.get(packet, to_string(field)) do
          nil ->
            de =
              Map.get(packet, :data_extended) ||
                Map.get(packet, "data_extended", %{}) || %{}

            Map.get(de, field) || Map.get(de, to_string(field)) || default

          val ->
            val
        end

      val ->
        val
    end
  end

  # Private helper functions

  @doc """
  Check if a packet is within the time threshold (not too old).
  """
  @spec packet_within_time_threshold?(struct(), any()) :: boolean()
  def packet_within_time_threshold?(%{received_at: received_at}, threshold) when not is_nil(received_at) do
    threshold_dt = convert_threshold_to_datetime(threshold)
    DateTime.compare(received_at, threshold_dt) in [:gt, :eq]
  end

  def packet_within_time_threshold?(_packet, _threshold), do: true

  defp convert_threshold_to_datetime(threshold) when is_integer(threshold) do
    # Assume seconds since epoch
    DateTime.from_unix!(threshold)
  end

  defp convert_threshold_to_datetime(threshold) when is_binary(threshold) do
    case DateTime.from_iso8601(threshold) do
      {:ok, dt, _} -> dt
      _ -> DateTime.utc_now()
    end
  end

  defp convert_threshold_to_datetime(%DateTime{} = threshold), do: threshold
  defp convert_threshold_to_datetime(_), do: DateTime.utc_now()

  @doc """
  Extracts symbol information from a packet with fallbacks.
  """
  @spec get_symbol_info(map()) :: {String.t(), String.t()}
  def get_symbol_info(packet) do
    AprsmeWeb.AprsSymbol.extract_from_packet(packet)
  end

  @doc """
  Extracts timestamp from a packet in ISO8601 format.
  """
  @spec get_timestamp(map()) :: String.t()
  def get_timestamp(packet) do
    received_at = Map.get(packet, :received_at) || Map.get(packet, "received_at")

    case received_at do
      nil ->
        ""

      %DateTime{} = dt ->
        DateTime.to_iso8601(dt)

      %NaiveDateTime{} = ndt ->
        ndt
        |> DateTime.from_naive!("Etc/UTC")
        |> DateTime.to_iso8601()

      timestamp when is_binary(timestamp) ->
        # If it's already a string, try to parse and reformat it
        case DateTime.from_iso8601(timestamp) do
          {:ok, dt, _} -> DateTime.to_iso8601(dt)
          _ -> timestamp
        end

      _ ->
        ""
    end
  end

  @doc """
  Generates a callsign string with SSID if present.
  """
  @spec generate_callsign(map()) :: String.t()
  def generate_callsign(packet) do
    base_callsign = get_packet_field(packet, :base_callsign, "")
    ssid = get_packet_field(packet, :ssid, nil)
    format_callsign_with_ssid(base_callsign, ssid)
  end

  @doc """
  Returns the display name for a packet.

  For APRS objects, returns the object_name.
  For APRS items, returns the item_name.
  Otherwise, falls back to the sender callsign.
  """
  @spec display_name(map()) :: String.t()
  def display_name(packet) do
    object_name = get_packet_field(packet, :object_name, nil)
    item_name = get_packet_field(packet, :item_name, nil)

    cond do
      non_empty_string?(object_name) -> String.trim(object_name)
      non_empty_string?(item_name) -> String.trim(item_name)
      true -> get_packet_field(packet, :sender, "")
    end
  end

  @doc """
  Get the map label for a packet.

  For objects and items, show the object/item name to distinguish them from
  the station that created them. For example, a repeater object should show
  the repeater name, not the station that's reporting it.

  For position packets, show the sender callsign.
  """
  @spec map_label(map()) :: String.t()
  def map_label(packet) do
    map_label_for(get_packet_field(packet, :data_type, ""), packet)
  end

  defp map_label_for("object", packet) do
    prefer_name(get_packet_field(packet, :object_name, ""), packet)
  end

  defp map_label_for("item", packet) do
    prefer_name(get_packet_field(packet, :item_name, ""), packet)
  end

  defp map_label_for(_data_type, packet), do: get_packet_field(packet, :sender, "")

  # Use `name` when non-empty, otherwise fall back to the packet sender.
  defp prefer_name("", packet), do: get_packet_field(packet, :sender, "")
  defp prefer_name(name, _packet), do: name

  defp non_empty_string?(value) when is_binary(value), do: String.trim(value) != ""
  defp non_empty_string?(_), do: false

  defp format_callsign_with_ssid(base_callsign, nil), do: base_callsign
  defp format_callsign_with_ssid(base_callsign, ""), do: base_callsign
  defp format_callsign_with_ssid(base_callsign, ssid), do: "#{base_callsign}-#{ssid}"
end
