defmodule Microwaveprop.Propagation.Eme do
  @moduledoc """
  Earth-Moon-Earth (moonbounce) link-budget helpers.

  EME path loss follows the bistatic radar equation with the Moon
  acting as a limb-darkened sphere of albedo ρ ≈ 0.065. That factors
  into a single RCS σ_moon ≈ 0.065 · π · R_moon² ≈ 6.16 × 10¹¹ m².

  Reducing the full radar equation to `f_MHz` and `d_km` gives the
  closed form used by this module:

      L_eme(dB) = -14.44 + 40·log₁₀(d_km) + 20·log₁₀(f_MHz)

  Spot-check values (round-trip, one leg Earth→Moon, one leg
  Moon→Earth):

  - 144 MHz at 384_400 km → ~252 dB
  - 432 MHz at 384_400 km → ~261 dB
  - 1_296 MHz at 384_400 km → ~271 dB
  - 10_368 MHz at 384_400 km → ~289 dB

  Pure functions only — no DB, no observer state.
  """

  @moon_radius_km 1738.1
  @default_albedo 0.065

  @doc """
  Band-dependent lunar radar albedo ρ (dimensionless).

  Measured / cited values from the EME literature (Evans 1969,
  Hagfors, and later K1JT / W5LUA handbook values):

  | Band             | ρ    |
  |------------------|------|
  | < 300 MHz        | 0.065|
  | 300–999 MHz      | 0.081|
  | 1–1.999 GHz      | 0.065|
  | 2–4.999 GHz      | 0.070|
  | 5–9.999 GHz      | 0.080|
  | 10–23.999 GHz    | 0.090|
  | 24–46.999 GHz    | 0.060|
  | 47–75.999 GHz    | 0.030|
  | ≥ 76 GHz         | 0.020|

  The tiers reflect the surface-roughness regime the radar beam
  probes: meter-wavelength bands see quasi-specular returns off
  smooth mare, mid-centimetre bands see a rough surface, and
  millimetre bands start losing reflectivity to thermal emission.
  """
  @spec moon_albedo(number()) :: float()
  def moon_albedo(freq_mhz) when is_number(freq_mhz) and freq_mhz < 300, do: 0.065
  def moon_albedo(freq_mhz) when freq_mhz < 1000, do: 0.081
  def moon_albedo(freq_mhz) when freq_mhz < 2000, do: 0.065
  def moon_albedo(freq_mhz) when freq_mhz < 5000, do: 0.070
  def moon_albedo(freq_mhz) when freq_mhz < 10_000, do: 0.080
  def moon_albedo(freq_mhz) when freq_mhz < 24_000, do: 0.090
  def moon_albedo(freq_mhz) when freq_mhz < 47_000, do: 0.060
  def moon_albedo(freq_mhz) when freq_mhz < 76_000, do: 0.030
  def moon_albedo(freq_mhz) when freq_mhz >= 76_000, do: 0.020

  @doc """
  Returns the Moon's radar cross-section σ in m² for a given albedo.
  Defaults to 0.065 (the mean value commonly quoted at 1.3 GHz) so
  callers that don't care about band-dependence get the classic
  baseline.
  """
  @spec moon_rcs_m2(number()) :: float()
  def moon_rcs_m2(albedo \\ @default_albedo) when is_number(albedo) and albedo > 0 do
    radius_m = @moon_radius_km * 1000.0
    albedo * :math.pi() * radius_m * radius_m
  end

  @doc """
  Free-space round-trip path loss (Earth → Moon → Earth) in dB,
  without any Moon-reflection term. This is just 2 × one-way FSPL:

      L_fs,2way(dB) = 64.90 + 40·log₁₀(f_MHz) + 40·log₁₀(d_km)

  Pair with `moon_reflection_gain_db/2` to decompose the full EME
  loss into its spreading vs scattering terms.
  """
  @spec fspl_round_trip_db(number(), number()) :: float()
  def fspl_round_trip_db(freq_mhz, distance_km)
      when is_number(freq_mhz) and freq_mhz > 0 and is_number(distance_km) and distance_km > 0 do
    64.90 + 40.0 * :math.log10(freq_mhz) + 40.0 * :math.log10(distance_km)
  end

  @doc """
  The Moon's equivalent "antenna gain" in dB for a monostatic radar:
  `G_moon = 10·log₁₀(4π σ / λ²)` where σ = ρ · π · R². This is the
  term that *reduces* the net round-trip path loss relative to plain
  double-FSPL — i.e. the moon's reflection effectiveness.

  `moon_reflection_gain_db/2` lets callers override the albedo (for
  example via `moon_albedo/1`); `moon_reflection_gain_db/1` defaults
  to ρ = 0.065.
  """
  @spec moon_reflection_gain_db(number(), number()) :: float()
  def moon_reflection_gain_db(freq_mhz, albedo \\ @default_albedo)
      when is_number(freq_mhz) and freq_mhz > 0 and is_number(albedo) and albedo > 0 do
    lambda_m = 300.0 / freq_mhz
    sigma = moon_rcs_m2(albedo)
    10.0 * :math.log10(4.0 * :math.pi() * sigma / (lambda_m * lambda_m))
  end

  @doc """
  Round-trip EME path loss in dB for a frequency in MHz and an
  Earth→Moon slant range in km. Covers the transmit leg, Moon
  scattering, and receive leg in a single number (the conventional
  "moonbounce path loss" that link-budget spreadsheets use).

  Accepts an optional `albedo` override — pass
  `moon_albedo(freq_mhz)` for a band-aware calculation. Defaults to
  ρ = 0.065, the classic 1296 MHz value, which keeps legacy callers
  stable.

  Equivalent to `fspl_round_trip_db - moon_reflection_gain_db`.
  """
  @spec path_loss_db(number(), number(), number()) :: float()
  def path_loss_db(freq_mhz, distance_km, albedo \\ @default_albedo)
      when is_number(freq_mhz) and freq_mhz > 0 and is_number(distance_km) and distance_km > 0 and is_number(albedo) and
             albedo > 0 do
    fspl_round_trip_db(freq_mhz, distance_km) - moon_reflection_gain_db(freq_mhz, albedo)
  end

  # Speed of light (m/s).
  @c_m_s 299_792_458.0

  @doc """
  Self-echo Doppler shift in Hz for an EME bounce, given the operating
  frequency and the Moon's radial velocity relative to the observer.

  For an own-echo round-trip the Doppler is twice the one-way shift:
  `Δf = −2 · f · v_radial / c`. A positive radial velocity (Moon
  receding) produces a negative Hz shift — the returned bounce lands
  *below* the transmit frequency.
  """
  @spec doppler_shift_hz(number(), number()) :: float()
  def doppler_shift_hz(freq_mhz, radial_velocity_km_s)
      when is_number(freq_mhz) and freq_mhz > 0 and is_number(radial_velocity_km_s) do
    freq_hz = freq_mhz * 1_000_000.0
    v_m_s = radial_velocity_km_s * 1000.0
    -2.0 * freq_hz * v_m_s / @c_m_s
  end

  @doc """
  Received signal power in dBm for a symmetric own-echo EME link
  (operator listens to their own bounce) given:

  - `:tx_power_dbm` — transmitter output power (dBm)
  - `:tx_gain_dbi` — transmit antenna gain (dBi)
  - `:rx_gain_dbi` — receive antenna gain (dBi) — same as tx for
    self-echo, kept distinct so the function also works for cross-
    station contacts
  - `:freq_mhz` — operating frequency (MHz)
  - `:distance_km` — one-way slant range to the Moon (km)
  """
  @spec received_power_dbm(keyword()) :: float()
  def received_power_dbm(opts) do
    tx = Keyword.fetch!(opts, :tx_power_dbm)
    gt = Keyword.fetch!(opts, :tx_gain_dbi)
    gr = Keyword.fetch!(opts, :rx_gain_dbi)
    f = Keyword.fetch!(opts, :freq_mhz)
    d = Keyword.fetch!(opts, :distance_km)

    tx + gt + gr - path_loss_db(f, d)
  end

  @doc """
  Thermal noise floor (dBm) in a given bandwidth and system noise
  temperature. Callers use this against `received_power_dbm/1` to
  get SNR.

  Default Tsys = 290 K (room-temperature receive system, a reasonable
  placeholder for VHF; a real cooled LNA at 10 GHz is closer to 100 K).
  """
  @spec noise_floor_dbm(number(), number()) :: float()
  def noise_floor_dbm(bandwidth_hz, t_sys_k \\ 290.0)
      when is_number(bandwidth_hz) and bandwidth_hz > 0 and is_number(t_sys_k) and t_sys_k > 0 do
    # -174 dBm/Hz + 10 log(BW) + 10 log(T / 290)
    -174.0 + 10.0 * :math.log10(bandwidth_hz) + 10.0 * :math.log10(t_sys_k / 290.0)
  end

  @doc """
  Signal-to-noise ratio (dB) for a symmetric own-echo EME link.
  Convenience on top of `received_power_dbm/1` and
  `noise_floor_dbm/2` — returns the dB margin over thermal noise in
  the specified detection bandwidth.
  """
  @spec snr_db(keyword()) :: float()
  def snr_db(opts) do
    bw_hz = Keyword.fetch!(opts, :bandwidth_hz)
    t_sys_k = Keyword.get(opts, :t_sys_k, 290.0)
    received_power_dbm(opts) - noise_floor_dbm(bw_hz, t_sys_k)
  end
end
