defmodule Microwaveprop.Propagation.SporadicE do
  @moduledoc """
  Sporadic-E propagation physics for VHF scoring.

  Given foEs (the E-layer critical frequency measured by an ionosonde,
  in MHz) and a ground-range distance in km, computes the single-hop
  Es MUF via the thin-layer obliquity factor from ITU-R P.534-6 /
  Davies 1990 *Ionospheric Radio*, Eq. 6.26:

      MUF_Es = foEs · sec(i)

  where `sec(i) = √(1 + (D / (2h))²)` with the reflection height
  `h = 110 km` (standard thin-E approximation).

  The `es_score/3` function maps a `(foEs, target band, distance)`
  triple into a 0–100 propagation likelihood for **single-hop Es
  only**. Multi-hop Es (which extends the range but needs consecutive
  intense Es patches) and Es-scatter are not modelled here — those
  are separate factors.

  Calibration notes:
  - 50 MHz Es at 2000 km needs foEs ≳ 5.5 MHz (routine summer Es).
  - 144 MHz Es at 2000 km needs foEs ≳ 15.8 MHz (rare, observed during
    major Jun/Jul peaks).
  - 222/432 MHz Es does not occur at physically observed foEs values.
  - Single-hop Es has a minimum useful range of ~500 km (steeper rays
    escape the layer rather than reflect) and a maximum of ~2500 km
    (the geometric horizon limit for a 110 km reflection height).
  """

  # Standard E-layer reflection height for the thin-layer approximation.
  @e_layer_height_km 110.0

  @min_hop_km 500
  @max_hop_km 2500

  @doc """
  Single-hop Es MUF in MHz. Returns 0.0 for nil/non-positive inputs.
  """
  @spec single_hop_muf(number() | nil, number() | nil) :: float()
  def single_hop_muf(nil, _), do: 0.0
  def single_hop_muf(_, nil), do: 0.0
  def single_hop_muf(fo_es_mhz, _) when fo_es_mhz <= 0, do: 0.0
  def single_hop_muf(_, distance_km) when distance_km <= 0, do: 0.0

  def single_hop_muf(fo_es_mhz, distance_km) do
    sec_i = :math.sqrt(1 + :math.pow(distance_km / (2 * @e_layer_height_km), 2))
    fo_es_mhz * sec_i
  end

  @doc """
  Returns a 0–100 score for single-hop Es propagation of `band_mhz`
  over a ground-range `distance_km`, given the measured `fo_es_mhz`.

  - Short paths (< 500 km) and long paths (> 2500 km) are outside the
    single-hop window → 0.
  - `nil` or 0 foEs → 0.
  - Otherwise the score ramps with `MUF_Es / band_mhz`:
      ≥ 1.20 → 100 (comfortably above the critical threshold)
      ≥ 1.00 → 80  (just above, marginal Es)
      ≥ 0.90 → 50  (close, occasional fringe)
      ≥ 0.70 → 20  (weak fringe, unreliable)
      below  → 0
  """
  @spec es_score(number() | nil, number(), number() | nil) :: integer()
  def es_score(nil, _band, _distance), do: 0
  def es_score(_fo_es, _band, nil), do: 0
  def es_score(fo_es_mhz, _band, _distance) when fo_es_mhz <= 0, do: 0

  def es_score(_fo_es, _band, distance_km) when distance_km < @min_hop_km or distance_km > @max_hop_km, do: 0

  def es_score(fo_es_mhz, band_mhz, distance_km) do
    muf = single_hop_muf(fo_es_mhz, distance_km)
    ratio = muf / band_mhz

    cond do
      ratio >= 1.20 -> 100
      ratio >= 1.00 -> 80
      ratio >= 0.90 -> 50
      ratio >= 0.70 -> 20
      true -> 0
    end
  end
end
