defmodule AprsmeWeb.Plugs.RemoteIp do
  @moduledoc """
  Plug that extracts the real client IP from Cloudflare or proxy headers
  and sets `conn.remote_ip` so downstream logging shows the actual client address.

  Header priority:
  1. `CF-Connecting-IP` (Cloudflare)
  2. `X-Forwarded-For` (first IP in the chain)

  ## Trust model

  Forwarded headers are only respected when the **TCP peer IP** (the actual
  connecting IP, before any header rewriting) is within a configured list of
  trusted proxy CIDRs. If the connecting peer is not trusted, the forwarded
  headers are ignored and the TCP peer IP is used as-is.

  Configure via Application environment or plug options:

      config :aprsme, AprsmeWeb.Plugs.RemoteIp,
        trusted_proxies: ["10.0.0.0/8", "172.16.0.0/12"]

  Or inline in the endpoint pipeline:

      plug AprsmeWeb.Plugs.RemoteIp, trusted_proxies: ["103.21.244.0/22"]

  When no trusted proxies are configured, **no peer is trusted** and all
  forwarded headers are ignored — the safest default.
  """
  @behaviour Plug

  import Plug.Conn

  require Logger

  @default_trusted_proxies []

  @impl true
  def init(opts) do
    trusted_proxies =
      Keyword.get(opts, :trusted_proxies) ||
        Application.get_env(:aprsme, __MODULE__, [])[:trusted_proxies] ||
        @default_trusted_proxies

    cidrs =
      trusted_proxies
      |> Enum.map(&InetCidr.parse_cidr/1)
      |> Enum.filter(fn
        {:ok, _cidr} ->
          true

        {:error, reason} ->
          Logger.warning("Invalid CIDR in RemoteIp trusted_proxies: #{inspect(reason)}")
          false
      end)
      |> Enum.map(fn {:ok, cidr} -> cidr end)

    %{trusted_proxies: cidrs}
  end

  @impl true
  def call(conn, %{trusted_proxies: cidrs}) do
    peer_ip = conn.remote_ip

    conn =
      if trusted_peer?(peer_ip, cidrs) do
        case get_client_ip(conn) do
          {:ok, ip} -> %{conn | remote_ip: ip}
          :error -> conn
        end
      else
        conn
      end

    user_agent =
      case get_req_header(conn, "user-agent") do
        [ua | _] -> ua
        [] -> "-"
      end

    Logger.metadata(
      remote_ip: conn.remote_ip |> :inet.ntoa() |> to_string(),
      user_agent: user_agent
    )

    conn
  end

  defp trusted_peer?(peer_ip, cidrs) do
    Enum.any?(cidrs, fn cidr -> InetCidr.contains?(cidr, peer_ip) end)
  end

  defp get_client_ip(conn) do
    with :error <- parse_cf_header(conn) do
      parse_forwarded_for(conn)
    end
  end

  defp parse_cf_header(conn) do
    case get_req_header(conn, "cf-connecting-ip") do
      [ip_str | _] -> parse_ip(ip_str)
      [] -> :error
    end
  end

  defp parse_forwarded_for(conn) do
    case get_req_header(conn, "x-forwarded-for") do
      [value | _] ->
        value
        |> String.split(",")
        |> List.first()
        |> parse_ip()

      [] ->
        :error
    end
  end

  defp parse_ip(ip_str) do
    case ip_str |> String.trim() |> String.to_charlist() |> :inet.parse_address() do
      {:ok, ip} -> {:ok, ip}
      {:error, _} -> :error
    end
  end
end
