defmodule Microwaveprop.Rover.PathTerrain do
  @moduledoc """
  Per-link terrain clearance for the rover scoring pipeline.

  For each (rover cell, fixed station) pair, samples elevation along the
  great-circle path between them and reports how far the rover sits
  above the highest intermediate terrain. Positive clearance means the
  rover should clear trees/buildings between it and the station; negative
  clearance means the path is blocked by an intervening ridge.
  """

  alias Microwaveprop.Buildings.Index, as: BuildingsIndex
  alias Microwaveprop.Canopy
  alias Microwaveprop.Rover.Elevation

  # Number of intermediate samples between rover and station.
  @sample_count 12

  # Search radius around each path sample point when looking for
  # buildings that obstruct the line of sight. ~150 m catches the
  # typical width of a downtown high-rise plus a margin for building
  # centroid imprecision.
  @building_search_radius_m 150

  @type latlon :: {float(), float()}

  @doc """
  Returns `%{ {rover_pt, station_pt} => clearance_m | nil }`.

  `clearance_m` is `rover_elev - max(intermediate_path_elev)`. `nil` when
  the rover cell or the entire intermediate path lacks SRTM coverage.
  """
  @spec clearance_map([map()], [map()], keyword()) :: %{{latlon(), latlon()} => integer() | nil}
  def clearance_map(cells, stations, opts \\ []) when is_list(cells) and is_list(stations) do
    elev_lookup = Keyword.get(opts, :elev_lookup, &Elevation.lookup_many/1)
    buildings_lookup = Keyword.get(opts, :buildings_lookup, &default_building_height/1)
    canopy_lookup = Keyword.get(opts, :canopy_lookup, &default_canopy_height/1)

    pairs =
      for cell <- cells, station <- stations do
        {{cell.lat, cell.lon}, {station.lat, station.lon}}
      end

    sample_points =
      pairs
      |> Enum.flat_map(fn {a, b} -> intermediate_samples(a, b) end)
      |> Enum.uniq()

    rover_points = Enum.map(cells, fn c -> {c.lat, c.lon} end)
    elev = elev_lookup.(rover_points ++ sample_points)

    Map.new(pairs, fn {rover_pt, station_pt} ->
      {{rover_pt, station_pt}, clearance(rover_pt, station_pt, elev, buildings_lookup, canopy_lookup)}
    end)
  end

  defp clearance(rover_pt, station_pt, elev, buildings_lookup, canopy_lookup) do
    rover_elev = Map.get(elev, rover_pt)

    case rover_elev do
      nil ->
        nil

      _ ->
        path_max =
          rover_pt
          |> intermediate_samples(station_pt)
          |> Enum.map(&obstacle_top(&1, elev, buildings_lookup, canopy_lookup))
          |> Enum.reject(&is_nil/1)
          |> Enum.max(fn -> nil end)

        if path_max, do: rover_elev - path_max
    end
  end

  defp obstacle_top({lat, lon} = pt, elev, buildings_lookup, canopy_lookup) do
    case Map.get(elev, pt) do
      nil ->
        nil

      ground ->
        building_m = buildings_lookup.({lat, lon})
        canopy_m = canopy_lookup.({lat, lon})
        ground + round(max(building_m, canopy_m))
    end
  end

  defp default_building_height({lat, lon}) do
    BuildingsIndex.max_height_near(lat, lon, @building_search_radius_m)
  end

  defp default_canopy_height({lat, lon}) do
    Canopy.lookup(lat, lon)
  end

  defp intermediate_samples({lat1, lon1}, {lat2, lon2}) do
    # Linear interpolation in lat/lon — fine at the distances we work with
    # (≤200 mi). Skip endpoints (i=0 is the rover, i=N+1 is the station).
    for i <- 1..@sample_count do
      f = i / (@sample_count + 1)
      {lat1 + f * (lat2 - lat1), lon1 + f * (lon2 - lon1)}
    end
  end
end
