defmodule Microwaveprop.Propagation.HfMuf do
  @moduledoc """
  HF / F2-layer MUF scoring from GIRO-measured MUFD(3000).

  GIRO ionosondes publish `MUFD` — the maximum usable frequency for a
  specific reference ground distance D (we request 3000 km). That is a
  *measured* value incorporating the true F2-layer profile and the CCIR
  M(3000)F2 climatology, not a thin-layer approximation.

  This module treats the measured MUFD as the anchor and scales it to
  the actual path distance using a thin-layer secant-of-incidence ratio
  (h ≈ 300 km). That preserves the calibration from the measurement and
  only uses the thin-layer formula for a *relative* distance correction,
  which is much more accurate than recomputing MUF from scratch.

  Limitations:

  - Single-hop only; paths beyond ~4000 km need multi-hop logic we
    don't implement here.
  - Uses the nearest ionosonde's MUFD regardless of how far away it is
    — for CONUS paths outside New England the F2 layer can look quite
    different from Millstone Hill / Alpena. Good enough for a first
    pass; a proper solution needs CCIR coefficient tables.
  - Ignores D-layer absorption, so low-frequency cutoff (LUF) is not
    modelled. The scoring curve is MUF-only.
  - Ignores geomagnetic disturbance (Kp) effects. A storm-elevated Kp
    can collapse daytime MUF without touching MUFD if the storm
    post-dates the nearest observation.
  """

  @f2_layer_height_km 300.0
  @ref_distance_km 3000.0

  # FOT (Frequency of Optimum Traffic) is the standard 85 % of MUF that
  # ITU-R / NOAA quote as the reliable working frequency below the MUF.
  @fot_ratio 0.85

  @doc """
  Scale a measured MUFD(3000) value to a different ground-range
  distance using thin-layer secant obliquity. Returns a MUF in MHz.

  At D = 3000 km this is a no-op (returns the input unchanged).
  """
  @spec adjust_mufd(number() | nil, number() | nil) :: float()
  def adjust_mufd(nil, _), do: 0.0
  def adjust_mufd(_, nil), do: 0.0
  def adjust_mufd(mufd, _) when mufd <= 0, do: 0.0
  def adjust_mufd(_, distance_km) when distance_km <= 0, do: 0.0

  def adjust_mufd(mufd_ref_mhz, distance_km) do
    ratio = sec_i_factor(distance_km) / sec_i_factor(@ref_distance_km)
    mufd_ref_mhz * ratio
  end

  @doc """
  Frequency of Optimum Traffic — the standard 85 % of MUF used as the
  reliable working frequency below the MUF ceiling.
  """
  @spec fot(number() | nil) :: float()
  def fot(nil), do: 0.0
  def fot(muf) when muf <= 0, do: 0.0
  def fot(muf_mhz), do: muf_mhz * @fot_ratio

  @doc """
  Score an HF band on a path with the given MUF, in the 0–100 scale
  used by the rest of the propagation engine.

  - band ≤ 0.75 × MUF → 100 (reliable, comfortably below FOT)
  - band ≤ 0.85 × MUF (FOT) → 80 (optimal working band)
  - band ≤ 1.00 × MUF → 50 (marginal, pushing the MUF ceiling)
  - band ≤ 1.15 × MUF → 20 (fringe, intermittent)
  - band > 1.15 × MUF → 0 (dead, well above MUF)
  """
  @spec hf_score(number() | nil, number()) :: integer()
  def hf_score(nil, _band), do: 0
  def hf_score(muf, _band) when muf <= 0, do: 0

  def hf_score(muf_mhz, band_mhz) do
    ratio = band_mhz / muf_mhz

    cond do
      ratio <= 0.75 -> 100
      ratio <= 0.85 -> 80
      ratio <= 1.00 -> 50
      ratio <= 1.15 -> 20
      true -> 0
    end
  end

  defp sec_i_factor(distance_km) do
    :math.sqrt(1 + :math.pow(distance_km / (2 * @f2_layer_height_km), 2))
  end
end
