defmodule Microwaveprop.Propagation.Duct do
  @moduledoc """
  Detects atmospheric ducts from a native-level HRRR profile by
  computing the modified refractivity M-profile and finding regions
  where dM/dh < 0.

  This replaces the scalar `min_refractivity_gradient` with physical
  duct geometry: base height, top height, thickness, M-deficit, and
  the minimum frequency the duct can trap. The per-band trapped
  frequency is the key improvement — a 50 m duct can trap 24 GHz but
  not 3 GHz, and the old scalar approach had no way to express that.

  References:
  - ITU-R P.453-14: refractivity formula N = 77.6*P/T + 3.73e5*e/T²
  - ITU-R P.834-9: ducting defined by regions where dM/dh < 0
  - Bean & Dutton (1966): minimum trapped wavelength λ_min ≈ ...
  """

  @doc """
  Compute the refractivity N at each level of a native profile.

  Returns `[{height_m, N}, ...]` sorted ascending by height.
  Uses ITU-R P.453 with water vapor pressure derived from specific
  humidity.
  """
  @spec refractivity_profile(map()) :: [{float(), float()}]
  def refractivity_profile(%{heights_m: heights, temp_k: temps, spfh: spfhs, pressure_pa: pressures}) do
    [heights, temps, spfhs, pressures]
    |> Enum.zip()
    |> Enum.map(fn {h, t, q, p} ->
      # Water vapor pressure: e = q*P / (0.622 + 0.378*q)  [Pa]
      e = q * p / (0.622 + 0.378 * q)
      # Convert to hPa for the P.453 formula
      p_hpa = p / 100.0
      e_hpa = e / 100.0

      n = 77.6 * p_hpa / t + 3.73e5 * e_hpa / (t * t)
      {h, n}
    end)
  end

  @doc """
  Convert an N-profile to the modified refractivity M-profile.

  M = N + 157 * h  (where h is in km)

  In a standard atmosphere M always increases with height. A duct
  exists wherever M decreases.
  """
  @spec m_profile([{float(), float()}]) :: [{float(), float()}]
  def m_profile(n_profile) do
    Enum.map(n_profile, fn {h, n} ->
      {h, n + 157.0 * h / 1000.0}
    end)
  end

  @doc """
  Detect ducts from an M-profile.

  A duct is a contiguous region where M decreases with height (dM/dh < 0).
  Returns a list of `%{base_m, top_m, thickness_m, m_deficit}` maps,
  where `m_deficit` is the total M decrease (positive number) across the
  duct — the strength of the trapping.
  """
  @spec detect_ducts([{float(), float()}]) ::
          [%{base_m: float(), top_m: float(), thickness_m: float(), m_deficit: float()}]
  def detect_ducts(m_profile) when length(m_profile) < 2, do: []

  def detect_ducts(m_profile) do
    m_profile
    |> Enum.chunk_every(2, 1, :discard)
    |> Enum.reduce({[], nil}, fn [{h1, m1}, {h2, m2}], acc ->
      process_m_pair(acc, h1, m1, h2, m2)
    end)
    |> close_trailing_duct()
  end

  defp process_m_pair({ducts, current_duct}, h1, m1, h2, m2) when m2 < m1 do
    # M is decreasing — we're in a duct
    duct = extend_or_start_duct(current_duct, h1, m1, h2, m2)
    {ducts, duct}
  end

  defp process_m_pair({ducts, nil}, _h1, _m1, _h2, _m2), do: {ducts, nil}

  defp process_m_pair({ducts, duct}, _h1, _m1, _h2, _m2) do
    {[finalize_duct(duct) | ducts], nil}
  end

  defp extend_or_start_duct(nil, h1, m1, h2, m2) do
    %{base_m: h1, base_m_val: m1, top_m: h2, min_m_val: m2}
  end

  defp extend_or_start_duct(duct, _h1, _m1, h2, m2) do
    %{duct | top_m: h2, min_m_val: min(duct.min_m_val, m2)}
  end

  defp close_trailing_duct({ducts, nil}), do: Enum.reverse(ducts)
  defp close_trailing_duct({ducts, duct}), do: Enum.reverse([finalize_duct(duct) | ducts])

  defp finalize_duct(%{base_m: base, top_m: top, base_m_val: m_base, min_m_val: m_min}) do
    %{
      base_m: base,
      top_m: top,
      thickness_m: top - base,
      m_deficit: m_base - m_min
    }
  end

  @doc """
  Minimum trapped frequency (GHz) for a duct of given thickness and M-deficit.

  Uses the waveguide approximation from Bean & Dutton:
    λ_max = 2.5 * d * sqrt(ΔM * 1e-6)  (meters)
    f_min = c / λ_max

  where d is duct thickness in meters, ΔM is the M-deficit.
  Returns a very high frequency (999 GHz) for degenerate ducts.
  """
  @spec min_trapped_frequency_ghz(%{thickness_m: float(), m_deficit: float()}) :: float()
  def min_trapped_frequency_ghz(%{thickness_m: d, m_deficit: delta_m}) when d > 0 and delta_m > 0 do
    # Maximum trapped wavelength (meters)
    lambda_max = 2.5 * d * :math.sqrt(delta_m * 1.0e-6)

    if lambda_max > 1.0e-6 do
      # f = c / λ, convert to GHz
      3.0e8 / lambda_max / 1.0e9
    else
      999.0
    end
  end

  def min_trapped_frequency_ghz(_), do: 999.0

  @doc """
  Full analysis pipeline: native profile → ducts with trapped frequencies.

  Returns `%{ducts: [duct_map], best_duct_band_ghz: float | nil}`
  where each duct_map includes `:min_freq_ghz` and
  `best_duct_band_ghz` is the lowest min_freq across all detected ducts.
  """
  @spec analyze(map()) ::
          %{
            ducts: [
              %{
                base_m: float(),
                top_m: float(),
                thickness_m: float(),
                m_deficit: float(),
                min_freq_ghz: float()
              }
            ],
            best_duct_band_ghz: float() | nil
          }
  def analyze(profile) do
    ducts =
      profile
      |> refractivity_profile()
      |> m_profile()
      |> detect_ducts()
      |> Enum.map(fn duct ->
        Map.put(duct, :min_freq_ghz, min_trapped_frequency_ghz(duct))
      end)

    best = ducts |> Enum.map(& &1.min_freq_ghz) |> Enum.min(fn -> nil end)

    %{ducts: ducts, best_duct_band_ghz: best}
  end
end
