defmodule Microwaveprop.Rover.CandidateDetail do
  @moduledoc """
  Server-side summary for a rover candidate cell: per-link distance,
  predicted SNR margin, terrain clearance, and the elevation profile
  along the great-circle path to each selected fixed station.
  """

  alias Microwaveprop.Buildings.Index, as: BuildingsIndex
  alias Microwaveprop.Canopy
  alias Microwaveprop.Radio.Maidenhead
  alias Microwaveprop.Rover.DriveTime
  alias Microwaveprop.Rover.LinkMargin
  alias Microwaveprop.Terrain.Srtm

  @sample_count 48

  # Search radius around each profile sample when checking buildings.
  # Slightly tighter than the path-clearance radius (150 m) because the
  # profile is a narrower visualization — we want what's "on the line".
  @profile_building_radius_m 80

  @type station :: %{callsign: String.t(), lat: float(), lon: float()}

  @type link_summary :: %{
          callsign: String.t(),
          distance_km: float(),
          bearing: String.t(),
          bearing_deg: float(),
          margin_db: float() | nil,
          rover_elev_m: integer() | nil,
          station_elev_m: integer() | nil,
          max_obstacle_m: integer() | nil,
          clearance_m: integer() | nil,
          profile: [%{dist_km: float(), elev: integer(), building_m: float(), canopy_m: integer()}]
        }

  @spec summarize(map(), [station()], non_neg_integer(), DateTime.t(), atom()) :: %{
          grid: String.t(),
          rover_elev_m: integer() | nil,
          links: [link_summary()]
        }
  def summarize(candidate, stations, band_mhz, valid_time, mode) do
    tiles_dir = Application.get_env(:microwaveprop, :srtm_tiles_dir, "/data/srtm")
    rover_elev = lookup_elev(candidate.lat, candidate.lon, tiles_dir)

    links =
      Enum.map(stations, fn s ->
        profile = profile_for(candidate, s, tiles_dir)

        max_obstacle =
          profile
          |> middle_samples()
          |> Enum.map(fn pt -> pt.elev + round(max(pt.building_m, pt.canopy_m)) end)
          |> Enum.max(fn -> nil end)

        clearance =
          if is_integer(rover_elev) and is_integer(max_obstacle),
            do: rover_elev - max_obstacle

        margin =
          LinkMargin.link_margin_db(
            band_mhz,
            valid_time,
            {candidate.lat, candidate.lon},
            {s.lat, s.lon},
            mode
          )

        from = {candidate.lat, candidate.lon}
        to = {s.lat, s.lon}

        %{
          callsign: s.callsign,
          distance_km: DriveTime.haversine_km(from, to),
          bearing: DriveTime.bearing_compass(from, to),
          bearing_deg: DriveTime.bearing_deg(from, to),
          margin_db: margin,
          rover_elev_m: rover_elev,
          station_elev_m: profile |> List.last() |> Map.get(:elev),
          max_obstacle_m: max_obstacle,
          clearance_m: clearance,
          profile: profile
        }
      end)

    %{
      grid: Maidenhead.from_latlon(candidate.lat, candidate.lon, 10),
      rover_elev_m: rover_elev,
      links: links
    }
  end

  defp profile_for(candidate, station, tiles_dir) do
    case Srtm.fetch_elevation_profile(
           candidate.lat,
           candidate.lon,
           station.lat,
           station.lon,
           tiles_dir,
           @sample_count
         ) do
      {:ok, points} ->
        Enum.map(points, fn pt ->
          %{
            dist_km: pt.dist_km,
            elev: pt.elev,
            building_m: BuildingsIndex.max_height_near(pt.lat, pt.lon, @profile_building_radius_m),
            canopy_m: Canopy.lookup(pt.lat, pt.lon)
          }
        end)

      _ ->
        []
    end
  end

  defp lookup_elev(lat, lon, tiles_dir) do
    case Srtm.lookup(lat, lon, tiles_dir) do
      {:ok, e} -> e
      _ -> nil
    end
  end

  defp middle_samples([]), do: []
  defp middle_samples([_]), do: []
  defp middle_samples([_ | rest]), do: Enum.drop(rest, -1)
end
