defmodule Microwaveprop.Terrain.ElevationClient do
  @moduledoc false

  alias Microwaveprop.Terrain.Srtm

  require Logger

  @batch_size 100

  @spec fetch_elevation_profile(float(), float(), float(), float(), pos_integer(), keyword()) ::
          {:ok, list(map())} | {:error, String.t()}
  def fetch_elevation_profile(lat1, lon1, lat2, lon2, n \\ 64, opts \\ []) do
    Microwaveprop.Instrument.span(
      [:elevation, :fetch_profile],
      %{sample_count: n, source: if(srtm_tiles_dir(), do: :srtm, else: :api)},
      fn ->
        case srtm_tiles_dir() do
          nil ->
            Logger.debug("SRTM tiles_dir not configured, using API")
            fetch_elevation_profile_api(lat1, lon1, lat2, lon2, n)

          tiles_dir ->
            srtm_opts = Keyword.merge([download: true], opts)
            Srtm.fetch_elevation_profile(lat1, lon1, lat2, lon2, tiles_dir, n, srtm_opts)
        end
      end
    )
  end

  defp fetch_elevation_profile_api(lat1, lon1, lat2, lon2, n) do
    pts = sample_path(lat1, lon1, lat2, lon2, n)
    dist_km = haversine_km(lat1, lon1, lat2, lon2)

    with {:error, e1} <- fetch_batched(pts, :open_meteo),
         {:error, e2} <- fetch_batched(pts, :open_topo) do
      {:error, "Elevation fetch failed: #{e1} | #{e2}"}
    else
      {:ok, elevs} ->
        profile =
          pts
          |> Enum.zip(elevs)
          |> Enum.map(fn {pt, elev} ->
            %{lat: pt.lat, lon: pt.lon, d: pt.d, elev: elev || 0.0, dist_km: pt.d * dist_km}
          end)

        {:ok, profile}
    end
  end

  defp srtm_tiles_dir, do: Application.get_env(:microwaveprop, :srtm_tiles_dir)

  def sample_path(lat1, lon1, lat2, lon2, n) do
    for i <- 0..n do
      f = i / n

      %{
        lat: lat1 + f * (lat2 - lat1),
        lon: lon1 + f * (lon2 - lon1),
        d: f
      }
    end
  end

  defp fetch_batched(pts, source) do
    pts
    |> Enum.chunk_every(@batch_size)
    |> Enum.reduce_while({:ok, []}, fn batch, {:ok, acc} ->
      case fetch_batch(source, batch) do
        {:ok, elevs} -> {:cont, {:ok, acc ++ elevs}}
        {:error, _} = err -> {:halt, err}
      end
    end)
  end

  defp fetch_batch(:open_meteo, batch) do
    lats = Enum.map_join(batch, ",", &Float.to_string(Float.round(&1.lat, 6)))
    lons = Enum.map_join(batch, ",", &Float.to_string(Float.round(&1.lon, 6)))
    url = "https://api.open-meteo.com/v1/elevation?latitude=#{lats}&longitude=#{lons}"

    case Req.get(url, req_options()) do
      {:ok, %{status: 200, body: %{"elevation" => elevs}}} when is_list(elevs) ->
        {:ok, elevs}

      {:ok, %{status: status}} ->
        {:error, "Open-Meteo HTTP #{status}"}

      {:error, reason} ->
        {:error, "Open-Meteo error: #{inspect(reason)}"}
    end
  end

  defp fetch_batch(:open_topo, batch) do
    locations =
      Enum.map_join(batch, "|", fn p ->
        "#{Float.round(p.lat, 6)},#{Float.round(p.lon, 6)}"
      end)

    url = "https://api.opentopodata.org/v1/srtm90m?locations=#{URI.encode(locations)}"

    case Req.get(url, req_options()) do
      {:ok, %{status: 200, body: %{"status" => "OK", "results" => results}}} ->
        {:ok, Enum.map(results, fn r -> r["elevation"] || 0.0 end)}

      {:ok, %{status: status}} ->
        {:error, "OpenTopo HTTP #{status}"}

      {:error, reason} ->
        {:error, "OpenTopo error: #{inspect(reason)}"}
    end
  end

  defp haversine_km(lat1, lon1, lat2, lon2) do
    dlat = deg_to_rad(lat2 - lat1)
    dlon = deg_to_rad(lon2 - lon1)
    rlat1 = deg_to_rad(lat1)
    rlat2 = deg_to_rad(lat2)

    a =
      :math.sin(dlat / 2) ** 2 +
        :math.cos(rlat1) * :math.cos(rlat2) * :math.sin(dlon / 2) ** 2

    2 * 6371.0 * :math.asin(:math.sqrt(a))
  end

  defp deg_to_rad(deg), do: deg * :math.pi() / 180

  defp req_options do
    defaults = [retry: &retry?/2, max_retries: 5, retry_delay: &retry_delay/1]
    overrides = Application.get_env(:microwaveprop, :elevation_req_options, [])
    Keyword.merge(defaults, overrides)
  end

  defp retry?(_request, response) do
    case response do
      %Req.Response{status: status} when status in [429, 500, 502, 503, 504] -> true
      %{__exception__: true} -> true
      _ -> false
    end
  end

  defp retry_delay(n) do
    base = Integer.pow(2, n) * 1_000
    jitter = :rand.uniform(1_000)
    base + jitter
  end
end
