defmodule Microwaveprop.Weather.HrrrProfileLookup do
  @moduledoc """
  Read-side lookups against the `hrrr_profiles` table for clients that
  cannot use the on-disk `Propagation.ProfilesFile` store — typically
  the dev environment (no NFS mount, no live grid pipeline) or any
  point query for a location the grid file doesn't cover.

  Uses the same ±0.07° spatial bounding-box tolerance that
  `Weather.find_nearest_hrrr/3` uses (one HRRR grid cell at 0.125° is
  ~0.07° at mid-latitudes). The 4-column `(lat, lon, valid_time)`
  unique index makes the bbox + ORDER BY (squared distance) plan
  index-only.
  """

  import Ecto.Query

  alias Microwaveprop.Repo
  alias Microwaveprop.Weather.HrrrProfile

  @default_tolerance_deg 0.07
  @default_limit 32
  # HRRR persists 13 standard pressure levels per profile (1000/925/
  # 850/700/500/400/300/250/200/150/100 mb plus 2 boundary surfaces).
  # Per-QSO HrrrFetchWorker rows sometimes land with a partial profile
  # (4–11 levels) when individual byte-range fetches fail. SkewtLive
  # only wants complete profiles — anything below this threshold draws
  # a stubby diagram and gets filtered out by default.
  @default_min_levels 13

  @doc """
  Distinct `valid_time`s with at least one HRRR profile within
  `tolerance_deg` of the point, sorted ascending. The `:limit` keeps
  the most recent N (then re-sorts ascending so the SkewtLive time
  selector renders left-to-right in chronological order).

  Options:
    * `:tolerance_deg` — default `#{@default_tolerance_deg}`
    * `:limit` — default `#{@default_limit}`
    * `:min_profile_levels` — minimum array length on `profile` (default
      `#{@default_min_levels}`). Use `0` to keep partial-profile rows.
  """
  @spec list_valid_times_near(float(), float(), keyword()) :: [DateTime.t()]
  def list_valid_times_near(lat, lon, opts \\ []) when is_number(lat) and is_number(lon) do
    tolerance = Keyword.get(opts, :tolerance_deg, @default_tolerance_deg)
    limit = Keyword.get(opts, :limit, @default_limit)
    min_levels = Keyword.get(opts, :min_profile_levels, @default_min_levels)

    query =
      from p in HrrrProfile,
        where:
          p.lat >= ^(lat - tolerance) and p.lat <= ^(lat + tolerance) and
            p.lon >= ^(lon - tolerance) and p.lon <= ^(lon + tolerance) and
            fragment("coalesce(cardinality(?), 0) >= ?", p.profile, ^min_levels),
        group_by: p.valid_time,
        order_by: [desc: p.valid_time],
        limit: ^limit,
        select: p.valid_time

    query
    |> Repo.all()
    |> Enum.map(&ensure_utc/1)
    |> Enum.sort(DateTime)
  end

  @doc """
  Returns the spatially closest HRRR profile within `tolerance_deg`
  for the requested `valid_time`, or `nil` if none. Returned shape
  matches what `Propagation.ProfilesFile.read_point/3` yields so
  SkewtLive can substitute one for the other transparently.

  Options:
    * `:tolerance_deg` — default `#{@default_tolerance_deg}`
    * `:min_profile_levels` — minimum array length on `profile` (default
      `#{@default_min_levels}`). Use `0` to keep partial-profile rows.
  """
  @spec read_point_near(DateTime.t(), float(), float(), keyword()) :: map() | nil
  def read_point_near(%DateTime{} = valid_time, lat, lon, opts \\ []) when is_number(lat) and is_number(lon) do
    tolerance = Keyword.get(opts, :tolerance_deg, @default_tolerance_deg)
    min_levels = Keyword.get(opts, :min_profile_levels, @default_min_levels)
    naive = DateTime.to_naive(valid_time)

    query =
      from p in HrrrProfile,
        where:
          p.valid_time == ^naive and
            p.lat >= ^(lat - tolerance) and p.lat <= ^(lat + tolerance) and
            p.lon >= ^(lon - tolerance) and p.lon <= ^(lon + tolerance) and
            fragment("coalesce(cardinality(?), 0) >= ?", p.profile, ^min_levels),
        # squared-Euclidean distance is monotonic with great-circle at
        # this scale and avoids a trig call per row
        order_by: [
          asc: fragment("(? - ?)*(? - ?) + (? - ?)*(? - ?)", p.lat, ^lat, p.lat, ^lat, p.lon, ^lon, p.lon, ^lon)
        ],
        limit: 1

    case Repo.one(query) do
      nil -> nil
      %HrrrProfile{} = row -> to_cell(row)
    end
  end

  defp to_cell(%HrrrProfile{} = row) do
    %{
      lat: row.lat,
      lon: row.lon,
      valid_time: ensure_utc(row.valid_time),
      profile: row.profile || [],
      hpbl_m: row.hpbl_m,
      pwat_mm: row.pwat_mm,
      surface_temp_c: row.surface_temp_c,
      surface_dewpoint_c: row.surface_dewpoint_c,
      surface_pressure_mb: row.surface_pressure_mb,
      surface_refractivity: row.surface_refractivity,
      min_refractivity_gradient: row.min_refractivity_gradient,
      ducting_detected: row.ducting_detected,
      duct_characteristics: row.duct_characteristics
    }
  end

  defp ensure_utc(%DateTime{time_zone: "Etc/UTC"} = dt), do: dt
  defp ensure_utc(%DateTime{} = dt), do: DateTime.shift_zone!(dt, "Etc/UTC")

  defp ensure_utc(%NaiveDateTime{} = ndt) do
    {:ok, dt} = DateTime.from_naive(ndt, "Etc/UTC")
    dt
  end
end
