defmodule Microwaveprop.Terrain.TerrainAnalysis do
  @moduledoc """
  Terrain diffraction analysis per ITU-R P.526-16.

  Implements:
  - Knife-edge diffraction loss (Eq. 31)
  - Deygout 3-edge method for multiple obstacles (Section 6)
  - Dynamic k-factor from refractivity gradient (Section 2)
  """

  @type elevation_point :: %{lat: float(), lon: float(), elev: float(), d: float(), dist_km: float()}

  @type analysis_point :: %{
          lat: float(),
          lon: float(),
          d: float(),
          elev: float(),
          dist_km: float(),
          idx: non_neg_integer(),
          beam: float(),
          eff_terrain: float(),
          bulge: float(),
          r1: float(),
          d1_m: float(),
          d2_m: float(),
          clearance: float(),
          f1_clear: float(),
          obstructed: boolean(),
          fresnel_penetrated: boolean()
        }

  @type analysis_result :: %{
          points: [analysis_point()],
          diffraction_db: number(),
          verdict: String.t(),
          k_factor: float(),
          max_elevation_m: float(),
          min_clearance_m: float(),
          obstructed_count: non_neg_integer(),
          fresnel_hit_count: non_neg_integer()
        }

  @earth_radius_m 6_371_000.0
  @earth_radius_km 6_371.0

  @doc """
  Analyse an elevation profile for LOS clearance, Fresnel zone penetration,
  and diffraction loss per ITU-R P.526-16.

  ## Options
    * `:ant_ht_a` - antenna height A in meters (default 0.0)
    * `:ant_ht_b` - antenna height B in meters (default 0.0)
    * `:k_factor` - effective earth radius factor (default 4/3, compute with `k_factor/1`)
  """
  @spec analyse([elevation_point()], float(), float(), keyword()) :: analysis_result()
  def analyse(profile, dist_km, freq_ghz, opts \\ []) do
    Microwaveprop.Instrument.span(
      [:terrain, :analyse],
      %{point_count: length(profile), dist_km: dist_km, freq_ghz: freq_ghz},
      fn -> do_analyse(profile, dist_km, freq_ghz, opts) end
    )
  end

  defp do_analyse(profile, dist_km, freq_ghz, opts) do
    ant_ht_a = Keyword.get(opts, :ant_ht_a, 0.0)
    ant_ht_b = Keyword.get(opts, :ant_ht_b, 0.0)
    k = Keyword.get(opts, :k_factor, 4 / 3)

    lambda_m = 0.3 / freq_ghz
    n = length(profile) - 1

    first = hd(profile)
    last = List.last(profile)
    elev1 = first.elev + ant_ht_a
    elev2 = last.elev + ant_ht_b

    # Build effective profile with earth curvature correction
    points =
      profile
      |> Enum.with_index()
      |> Enum.map(fn {p, i} ->
        frac = i / n
        d1_m = frac * dist_km * 1000
        d2_m = (1 - frac) * dist_km * 1000

        beam = beam_height(frac, elev1, elev2)
        bulge = earth_bulge(frac, dist_km, k)
        eff_terrain = p.elev + bulge
        r1 = fresnel_radius(d1_m, d2_m, lambda_m)
        clearance = beam - eff_terrain
        f1_clear = if r1 > 0, do: clearance - r1, else: clearance

        is_endpoint = i == 0 or i == n

        %{
          lat: p.lat,
          lon: p.lon,
          d: p.d,
          elev: p.elev,
          dist_km: p.dist_km,
          idx: i,
          beam: beam,
          eff_terrain: eff_terrain,
          bulge: bulge,
          r1: r1,
          d1_m: d1_m,
          d2_m: d2_m,
          clearance: clearance,
          f1_clear: f1_clear,
          obstructed: not is_endpoint and clearance < 0,
          fresnel_penetrated: not is_endpoint and r1 > 0 and f1_clear < 0 and clearance >= 0
        }
      end)

    interior = Enum.slice(points, 1..-2//1)
    obstructed = Enum.filter(interior, & &1.obstructed)
    fresnel_hit = Enum.filter(interior, & &1.fresnel_penetrated)

    # Deygout diffraction: compute loss across all interior points
    diffraction_db = deygout_diffraction(interior, lambda_m)

    verdict = classify_verdict(diffraction_db, obstructed, fresnel_hit)

    elev_vals = Enum.map(profile, & &1.elev)
    clearance_vals = Enum.map(interior, & &1.clearance)
    max_elevation_m = Enum.max(elev_vals, fn -> 0.0 end)
    min_clearance_m = if clearance_vals == [], do: 999.0, else: Enum.min(clearance_vals)

    %{
      points: points,
      diffraction_db: diffraction_db,
      verdict: verdict,
      k_factor: k,
      max_elevation_m: max_elevation_m,
      min_clearance_m: min_clearance_m,
      obstructed_count: length(obstructed),
      fresnel_hit_count: length(fresnel_hit)
    }
  end

  @doc "First Fresnel zone radius at a point between two antennas."
  @spec fresnel_radius(number(), number(), number()) :: number()
  def fresnel_radius(d1_m, d2_m, lambda_m) do
    if d1_m <= 0 or d2_m <= 0 do
      0
    else
      :math.sqrt(lambda_m * d1_m * d2_m / (d1_m + d2_m))
    end
  end

  @doc "Earth bulge correction in meters at fractional distance along path."
  @spec earth_bulge(float(), float(), float()) :: float()
  def earth_bulge(frac, dist_km, k \\ 4 / 3) do
    d1 = frac * dist_km * 1000
    d2 = (1 - frac) * dist_km * 1000
    d1 * d2 / (2 * k * @earth_radius_m)
  end

  @doc """
  ITU-R P.526-16 Eq. 31 — knife-edge diffraction loss in dB.

  J(ν) = 6.9 + 20·log10(√((ν−0.1)² + 1) + ν − 0.1)  for ν > −0.78
  J(ν) = 0                                                for ν ≤ −0.78
  """
  @spec knife_edge_loss(number()) :: number()
  def knife_edge_loss(v) when v <= -0.78, do: 0

  def knife_edge_loss(v) do
    inner = :math.sqrt((v - 0.1) * (v - 0.1) + 1) + v - 0.1
    max(0, 6.9 + 20 * :math.log10(inner))
  end

  @doc """
  ITU-R P.526-16 diffraction parameter ν.

  ν = h · √(2·(d1+d2) / (λ·d1·d2))

  where h is height above direct ray (positive = above/blocked, negative = below/clear).
  """
  @spec diffraction_param(number(), number(), number(), number()) :: float()
  def diffraction_param(h, d1_m, d2_m, lambda_m) do
    if d1_m <= 0 or d2_m <= 0 or lambda_m <= 0 do
      0.0
    else
      h * :math.sqrt(2 * (d1_m + d2_m) / (lambda_m * d1_m * d2_m))
    end
  end

  @doc """
  Compute effective earth radius factor k from refractivity gradient dN/dh (N-units/km).

  k = 1 / (1 + a·(dN/dh)·10⁻⁶)

  Standard atmosphere: dN/dh ≈ −39 → k ≈ 4/3.
  Returns 4/3 for nil input.
  """
  @spec k_factor(number() | nil) :: float()
  def k_factor(nil), do: 4 / 3

  def k_factor(dn_dh) do
    denominator = 1 + @earth_radius_km * dn_dh * 1.0e-6

    if denominator <= 0.01 do
      # Near or beyond ducting — cap at very large k
      100.0
    else
      1.0 / denominator
    end
  end

  # ── Deygout 3-edge diffraction (P.526-16 Section 6) ─────────────

  defp deygout_diffraction(interior, lambda_m) do
    if interior == [] do
      0
    else
      # Find principal edge: point with highest ν on full T→R path
      principal = find_principal_edge(interior, lambda_m)

      if principal == nil or principal.nu <= -0.78 do
        0
      else
        loss_main = knife_edge_loss(principal.nu)

        # Find subsidiary edges on each sub-path
        {before_points, after_points} = split_at_edge(interior, principal.idx)

        loss_t = subsidiary_loss(before_points, lambda_m)
        loss_r = subsidiary_loss(after_points, lambda_m)

        max(0, loss_main + loss_t + loss_r)
      end
    end
  end

  defp find_principal_edge(points, lambda_m) do
    points
    |> Enum.map(fn p ->
      # h = height above direct ray = -clearance (clearance negative when above)
      h = -p.clearance
      nu = diffraction_param(h, p.d1_m, p.d2_m, lambda_m)
      %{idx: p.idx, nu: nu, d1_m: p.d1_m, d2_m: p.d2_m}
    end)
    |> Enum.max_by(& &1.nu, fn -> nil end)
  end

  defp split_at_edge(points, edge_idx) do
    before = Enum.filter(points, fn p -> p.idx < edge_idx end)
    after_pts = Enum.filter(points, fn p -> p.idx > edge_idx end)
    {before, after_pts}
  end

  defp subsidiary_loss([], _lambda_m), do: 0

  defp subsidiary_loss(points, lambda_m) do
    # Recompute ν relative to the sub-path endpoints
    # For sub-path T→principal or principal→R, the beam line is different
    # Use the existing d1_m/d2_m which are relative to the full path endpoints
    # This is a simplified Deygout where we use the same reference line
    sub_edge = find_principal_edge(points, lambda_m)

    if sub_edge == nil or sub_edge.nu <= -0.78 do
      0
    else
      knife_edge_loss(sub_edge.nu)
    end
  end

  # ── Verdict classification ──────────────────────────────────────

  defp classify_verdict(diffraction_db, obstructed, fresnel_hit) do
    cond do
      obstructed != [] -> "BLOCKED"
      fresnel_hit != [] and diffraction_db > 3 -> "FRESNEL_PARTIAL"
      fresnel_hit != [] -> "FRESNEL_MINOR"
      true -> "CLEAR"
    end
  end

  defp beam_height(frac, elev1, elev2) do
    elev1 + frac * (elev2 - elev1)
  end
end
