defmodule Microwaveprop.Buildings.Parser do
  @moduledoc """
  Streaming parser for Microsoft Building Footprint csv.gz tiles.

  Each line is a GeoJSON `Feature` whose `geometry.coordinates` is a
  Polygon (or MultiPolygon) and whose `properties.height` is meters
  above ground. We collapse each polygon to a centroid + bounding
  radius — that's all the rover path-clearance scorer needs, and it
  keeps the in-memory footprint to ~32 bytes per building.

  Polygons with `height = -1.0` (no estimate from the ML model) are
  dropped — including them would just add noise.
  """

  @type record :: %{
          centroid_lat: float(),
          centroid_lon: float(),
          max_radius_m: float(),
          height_m: float()
        }

  @doc """
  Returns a `Stream` of `t:record/0` for every polygon in `path` whose
  height is known. Lazy; suitable for tiles with millions of rows.
  """
  @spec parse_tile(String.t()) :: Enumerable.t()
  def parse_tile(path) do
    path
    |> File.stream!([:read, :compressed])
    |> Stream.flat_map(&parse_line/1)
  end

  defp parse_line(line) do
    case Jason.decode(String.trim(line)) do
      {:ok, %{"properties" => %{"height" => h}, "geometry" => geom}} when h > 0.0 ->
        case ring_from_geometry(geom) do
          [] -> []
          ring -> [build_record(ring, h)]
        end

      _ ->
        []
    end
  end

  defp ring_from_geometry(%{"type" => "Polygon", "coordinates" => [outer | _]}), do: outer
  defp ring_from_geometry(%{"type" => "MultiPolygon", "coordinates" => [[outer | _] | _]}), do: outer
  defp ring_from_geometry(_), do: []

  defp build_record(ring, height) do
    {sum_lat, sum_lon, n, min_lat, min_lon, max_lat, max_lon} =
      Enum.reduce(ring, {0.0, 0.0, 0, 90.0, 180.0, -90.0, -180.0}, fn
        [lon, lat], {sl, slon, n, mnla, mnlo, mxla, mxlo} ->
          {
            sl + lat,
            slon + lon,
            n + 1,
            min(mnla, lat),
            min(mnlo, lon),
            max(mxla, lat),
            max(mxlo, lon)
          }
      end)

    centroid_lat = sum_lat / n
    centroid_lon = sum_lon / n

    # Approximate max radius: half-diagonal of the bbox in meters.
    dlat_m = (max_lat - min_lat) * 111_000.0 / 2.0
    dlon_m = (max_lon - min_lon) * 111_000.0 * :math.cos(centroid_lat * :math.pi() / 180.0) / 2.0

    %{
      centroid_lat: centroid_lat,
      centroid_lon: centroid_lon,
      max_radius_m: :math.sqrt(dlat_m * dlat_m + dlon_m * dlon_m),
      height_m: height * 1.0
    }
  end
end
