defmodule Aprsme.PlausibleAnalytics do
  @moduledoc """
  Server-side event tracking for Plausible Analytics (self-hosted at a.w5isp.com).

  Fires an async HTTP POST to the Plausible Events API. All calls are fire-and-forget
  — analytics must never block or error the caller.

  ## Configuration

      config :aprsme, :plausible_analytics,
        endpoint: "https://a.w5isp.com/api/event",
        domain: "aprs.me"

  ## Usage

      PlausibleAnalytics.track(conn, "ws_subscribe_bounds")
      PlausibleAnalytics.track(conn, "api_callsign_lookup", %{callsign: "N0CALL"})
  """

  require Logger

  @doc """
  Track a custom event via the Plausible Events API.

  `conn_or_metadata` accepts either a `Plug.Conn` (for HTTP requests) or a map with
  `:remote_ip` / `:user_agent` / `:request_path` keys (for WebSocket channels).
  """
  @spec track(Plug.Conn.t() | map(), String.t(), map()) :: :ok
  def track(conn_or_metadata, event_name, props \\ %{})

  def track(%Plug.Conn{} = conn, event_name, props) do
    metadata = %{
      user_agent: first_header(conn, "user-agent"),
      remote_ip: remote_ip_string(conn),
      request_path: conn.request_path
    }

    do_track(metadata, event_name, props)
  end

  def track(%{} = metadata, event_name, props) do
    do_track(metadata, event_name, props)
  end

  defp do_track(metadata, event_name, props) do
    _task =
      Task.start(fn ->
        send_event(metadata, event_name, props)
      end)

    :ok
  end

  defp send_event(metadata, event_name, props) do
    config = config()

    url = "https://aprs.me#{Map.get(metadata, :request_path, "/")}"

    body = maybe_add_props(%{name: event_name, domain: config[:domain], url: url}, props)

    headers =
      maybe_add_forwarded_for(
        [{"content-type", "application/json"}, {"user-agent", Map.get(metadata, :user_agent, "aprs.me-server")}],
        Map.get(metadata, :remote_ip) || Map.get(metadata, "remote_ip")
      )

    case Req.post(config[:endpoint], json: body, headers: headers, max_retries: 0, receive_timeout: 2_000) do
      {:ok, %{status: status}} when status in 200..299 ->
        Logger.debug("Plausible event tracked: #{event_name}")

      {:ok, %{status: status}} ->
        Logger.warning("Plausible event rejected (#{status}): #{event_name}")

      {:error, error} ->
        Logger.warning("Plausible event failed: #{event_name} — #{inspect(error)}")
    end
  end

  defp maybe_add_props(body, props) when map_size(props) > 0 do
    Map.put(body, :props, props)
  end

  defp maybe_add_props(body, _props), do: body

  defp maybe_add_forwarded_for(headers, nil), do: headers

  defp maybe_add_forwarded_for(headers, ip) do
    [{"x-forwarded-for", ip} | headers]
  end

  defp first_header(conn, name) do
    conn
    |> Plug.Conn.get_req_header(name)
    |> List.first()
  end

  # Extract remote_ip as a string, handling the tuple format from Plug.Conn
  defp remote_ip_string(conn) do
    case conn.remote_ip do
      {a, b, c, d} -> "#{a}.#{b}.#{c}.#{d}"
      other -> to_string(other)
    end
  end

  defp config do
    Application.get_env(:aprsme, :plausible_analytics, [])
  end
end
