defmodule Aprsme.AprsIsConnection do
  @moduledoc """
  Maintains a supervised TCP connection to APRS-IS, with reconnection logic and
  exponential backoff. Broadcasts each received line via Phoenix.PubSub and emits
  telemetry events for connection, disconnection, errors, and packet receipt.
  """
  use GenServer

  alias Aprsme.LogSanitizer

  require Logger

  @type state :: %{
          socket: port() | nil,
          backoff: non_neg_integer()
        }

  @reconnect_initial 2_000
  @reconnect_max 60_000
  @pubsub_topic "aprs_is:raw"

  # Public API
  @spec start_link(Keyword.t()) :: GenServer.on_start()
  def start_link(_opts) do
    GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
  end

  @doc """
  Send a raw string to APRS-IS.
  """
  @spec send_packet(String.t()) :: :ok | {:error, term()}
  def send_packet(packet) do
    GenServer.call(__MODULE__, {:send, packet})
  end

  # GenServer callbacks
  @impl true
  def init(state) do
    # Check if APRS connection is disabled (e.g., in test mode)
    disable_connection = Application.get_env(:aprsme, :disable_aprs_connection, false)

    if disable_connection do
      Logger.info("APRS-IS connection disabled (test mode)")
      {:ok, Map.merge(%{socket: nil, backoff: @reconnect_initial}, state)}
    else
      schedule_connect(0)
      {:ok, Map.merge(%{socket: nil, backoff: @reconnect_initial}, state)}
    end
  end

  @impl true
  def handle_info(:connect, state) do
    case connect_aprs_is() do
      {:ok, socket} ->
        Logger.info("Connected to APRS-IS")
        :telemetry.execute([:aprsme, :is, :connected], %{}, %{})
        {:noreply, %{state | socket: socket, backoff: @reconnect_initial}}

      {:error, reason} ->
        Logger.error("APRS-IS connection failed, retrying with backoff",
          connection_error:
            LogSanitizer.log_data(
              reason: reason,
              backoff_ms: state.backoff,
              next_attempt: DateTime.add(DateTime.utc_now(), state.backoff, :millisecond)
            )
        )

        :telemetry.execute([:aprsme, :is, :connect_error], %{}, %{reason: reason})
        schedule_connect(state.backoff)
        {:noreply, %{state | socket: nil, backoff: min(state.backoff * 2, @reconnect_max)}}
    end
  end

  @impl true
  def handle_info({:tcp, _socket, data}, state) do
    # Each line received from APRS-IS
    Phoenix.PubSub.broadcast(Aprsme.PubSub, @pubsub_topic, {:aprsme_is_line, data})
    :telemetry.execute([:aprsme, :is, :packet], %{size: byte_size(data)}, %{data: data})
    {:noreply, state}
  end

  @impl true
  def handle_info({:tcp_closed, _socket}, state) do
    Logger.warning("APRS-IS connection closed, initiating reconnection",
      connection_status:
        LogSanitizer.log_data(
          event: "connection_closed",
          reconnect_delay_ms: @reconnect_initial,
          backoff_reset: true
        )
    )

    :telemetry.execute([:aprsme, :is, :disconnected], %{}, %{})
    schedule_connect(@reconnect_initial)
    {:noreply, %{state | socket: nil, backoff: @reconnect_initial}}
  end

  @impl true
  def handle_info({:tcp_error, _socket, reason}, state) do
    Logger.error("APRS-IS TCP error detected, reconnecting",
      tcp_error:
        LogSanitizer.log_data(
          reason: reason,
          reconnect_delay_ms: @reconnect_initial,
          socket_reset: true
        )
    )

    :telemetry.execute([:aprsme, :is, :tcp_error], %{}, %{reason: reason})
    schedule_connect(@reconnect_initial)
    {:noreply, %{state | socket: nil, backoff: @reconnect_initial}}
  end

  @impl true
  def handle_call({:send, packet}, _from, %{socket: socket} = state) when is_port(socket) do
    :ok = :gen_tcp.send(socket, packet <> "\r\n")
    {:reply, :ok, state}
  end

  def handle_call({:send, _packet}, _from, state) do
    {:reply, {:error, :not_connected}, state}
  end

  @impl true
  def terminate(_reason, %{socket: socket}) when is_port(socket) do
    :gen_tcp.close(socket)
    :ok
  end

  def terminate(_reason, _state), do: :ok

  defp schedule_connect(delay) do
    Process.send_after(self(), :connect, delay)
  end

  defp connect_aprs_is do
    host = Application.get_env(:aprsme, :aprsme_is_host, ~c"rotate.aprs2.net")
    port = Application.get_env(:aprsme, :aprsme_is_port, 14_580)
    callsign = Application.get_env(:aprsme, :aprsme_is_callsign, "N0CALL")
    passcode = Application.get_env(:aprsme, :aprsme_is_passcode, "00000")
    filter = Application.get_env(:aprsme, :aprsme_is_filter, "")

    opts = [:binary, active: true, packet: :line, keepalive: true]

    case :gen_tcp.connect(host, port, opts) do
      {:ok, socket} ->
        login = "user #{callsign} pass #{passcode} vers aprs.me 0.1 #{filter}\r\n"
        :ok = :gen_tcp.send(socket, login)
        {:ok, socket}

      error ->
        error
    end
  end

  @impl true
  def code_change(_old_vsn, state, _extra) do
    {:ok, state}
  end
end
