defmodule AprsmeWeb.Plugs.ContentSecurityPolicy do
  @moduledoc """
  Sets Content-Security-Policy headers with per-request nonces for script-src.

  Generates a cryptographically random nonce for each request, stores it in
  `conn.private[:csp_nonce]` so templates can apply it to inline `<script>` tags,
  and emits a `Content-Security-Policy` response header.

  External sources are limited to what the application actually uses:
  - Map tiles (OpenStreetMap, CARTO)
  - Plausible analytics (self-hosted)
  - Leaflet/plugin CDNs (unpkg, jsDelivr, cdnjs)
  - Sentry error tracking
  """

  import Plug.Conn

  @external_script_sources [
    "https://a.w5isp.com",
    "https://js.sentry-cdn.com",
    "https://unpkg.com",
    "https://cdn.jsdelivr.net",
    "https://cdnjs.cloudflare.com"
  ]

  @external_style_sources [
    "https://unpkg.com"
  ]

  @external_connect_sources [
    "wss:",
    "https://*.ingest.sentry.io",
    "https://*.sentry.io",
    "https://nominatim.openstreetmap.org",
    "https://tile.openstreetmap.org",
    "https://*.tile.openstreetmap.org",
    "https://*.tile.openstreetmap.de",
    "https://*.basemaps.cartocdn.com",
    "https://a.w5isp.com"
  ]

  @nonce_bytes 16

  @doc false
  def init(opts), do: opts

  @doc false
  def call(conn, _opts) do
    nonce = generate_nonce()
    conn = put_private(conn, :csp_nonce, nonce)

    csp = build_csp(nonce)

    conn
    |> delete_resp_header("content-security-policy")
    |> put_resp_header("content-security-policy", csp)
  end

  defp generate_nonce do
    @nonce_bytes
    |> :crypto.strong_rand_bytes()
    |> Base.encode64()
  end

  defp build_csp(nonce) do
    Enum.join(
      [
        "default-src 'self'",
        "script-src 'self' 'nonce-#{nonce}' #{Enum.join(@external_script_sources, " ")}",
        "style-src 'self' 'unsafe-inline' #{Enum.join(@external_style_sources, " ")}",
        "img-src 'self' data: https: http: blob:",
        "font-src 'self' data:",
        "connect-src 'self' #{Enum.join(@external_connect_sources, " ")}",
        "media-src 'self'",
        "object-src 'none'",
        "frame-ancestors 'none'",
        "base-uri 'self'",
        "form-action 'self'",
        "frame-src 'self'",
        "manifest-src 'self'",
        "worker-src 'self' blob:"
      ],
      "; "
    )
  end
end
