defmodule Microwaveprop.Commercial do
  @moduledoc false

  import Ecto.Query

  alias Microwaveprop.Commercial.Link
  alias Microwaveprop.Commercial.Sample
  alias Microwaveprop.Repo

  @default_radius_km 75.0
  @default_baseline_days 7
  @default_current_window_seconds 900
  @min_baseline_samples 5

  @spec enabled_links() :: [Link.t()]
  def enabled_links do
    Link
    |> where([l], l.enabled == true)
    |> Repo.all()
  end

  @doc """
  Returns the aggregate commercial-link rx_power degradation at a location,
  or `nil` if no healthy links are in range.

  Commercial LOS microwave links near the target point act as an inverse
  tropo sensor: when their rx_power drops significantly below the 7-day
  baseline without an obvious equipment cause (link_state still 1), the
  refractivity structure that hurts their short Fresnel zone is usually
  the same structure that *helps* long beyond-LOS amateur propagation.

  Result map:
    %{
      degradation_db: float (positive means worse than baseline),
      baseline_dbm: float,
      current_dbm: float,
      n_links: integer
    }

  ## Options

    * `:radius_km` — distance from the target point to include a link (default 75)
    * `:baseline_days` — days of history for the baseline average (default 7)
    * `:current_window_seconds` — how recent "current" rx must be (default 900)
  """
  @spec link_degradation_at({float(), float()}, DateTime.t(), keyword()) :: map() | nil
  def link_degradation_at({lat, lon}, valid_time, opts \\ []) do
    lookup = build_link_lookup(valid_time, opts)
    link_degradation_from_lookup({lat, lon}, lookup, opts)
  end

  @doc """
  Precompute every enabled link's degradation once so a bulk caller
  (like PropagationGridWorker's per-cell merge) can reuse the same
  `(link, endpoint, degradation)` list across all 95k grid points
  instead of re-running `enabled_links/0` and the per-link sample
  queries per call. Result is fed back into
  `link_degradation_from_lookup/3`.
  """
  @spec build_link_lookup(DateTime.t(), keyword()) :: [
          {Link.t(), {float(), float()}, map() | nil}
        ]
  def build_link_lookup(valid_time, opts \\ []) do
    baseline_days = Keyword.get(opts, :baseline_days, @default_baseline_days)
    current_window = Keyword.get(opts, :current_window_seconds, @default_current_window_seconds)

    baseline_cutoff = DateTime.add(valid_time, -baseline_days * 24 * 3600, :second)
    current_cutoff = DateTime.add(valid_time, -current_window, :second)

    enabled_links()
    |> Enum.map(fn link -> {link, link_endpoint(link)} end)
    |> Enum.reject(fn {_link, endpoint} -> is_nil(endpoint) end)
    |> Enum.map(fn {link, endpoint} ->
      degradation = link_degradation(link.id, baseline_cutoff, current_cutoff, valid_time)
      {link, endpoint, degradation}
    end)
  end

  @doc """
  Pure per-cell aggregator — given a `(lat, lon)` point and a
  precomputed lookup from `build_link_lookup/2`, returns the
  aggregate degradation map or `nil` when no in-range link is
  reporting usable data.
  """
  @spec link_degradation_from_lookup({float(), float()}, [tuple()], keyword()) :: map() | nil
  def link_degradation_from_lookup({lat, lon}, lookup, opts \\ []) do
    radius_km = Keyword.get(opts, :radius_km, @default_radius_km)

    results =
      for {_link, {elat, elon}, %{} = degradation} <- lookup,
          haversine_km(lat, lon, elat, elon) <= radius_km do
        degradation
      end

    aggregate_degradation(results)
  end

  defp aggregate_degradation([]), do: nil

  defp aggregate_degradation(list) do
    baseline_avg = average(Enum.map(list, & &1.baseline_dbm))
    current_avg = average(Enum.map(list, & &1.current_dbm))

    %{
      degradation_db: Float.round(baseline_avg - current_avg, 2),
      baseline_dbm: Float.round(baseline_avg, 2),
      current_dbm: Float.round(current_avg, 2),
      n_links: length(list)
    }
  end

  defp link_degradation(link_id, baseline_cutoff, current_cutoff, valid_time) do
    baseline =
      Sample
      |> where([s], s.link_id == ^link_id)
      |> where([s], s.sampled_at >= ^baseline_cutoff and s.sampled_at < ^current_cutoff)
      |> where([s], s.link_state == 1 and not is_nil(s.rx_power_0))
      |> select([s], avg(s.rx_power_0))
      |> Repo.one()

    {current, baseline_n} = fetch_current_and_baseline_count(link_id, current_cutoff, valid_time)

    cond do
      is_nil(baseline) or is_nil(current) ->
        nil

      baseline_n < @min_baseline_samples ->
        nil

      true ->
        %{baseline_dbm: to_float(baseline), current_dbm: current}
    end
  end

  defp to_float(%Decimal{} = d), do: Decimal.to_float(d)
  defp to_float(n) when is_number(n), do: n * 1.0

  defp fetch_current_and_baseline_count(link_id, current_cutoff, valid_time) do
    current =
      Sample
      |> where([s], s.link_id == ^link_id)
      |> where([s], s.sampled_at >= ^current_cutoff and s.sampled_at <= ^valid_time)
      |> where([s], s.link_state == 1 and not is_nil(s.rx_power_0))
      |> order_by([s], desc: s.sampled_at)
      |> limit(1)
      |> select([s], s.rx_power_0)
      |> Repo.one()

    baseline_n =
      Sample
      |> where([s], s.link_id == ^link_id)
      |> where([s], s.sampled_at < ^current_cutoff)
      |> where([s], s.link_state == 1 and not is_nil(s.rx_power_0))
      |> select([s], count(s.id))
      |> Repo.one()

    {current, baseline_n}
  end

  defp link_endpoint(%Link{endpoint_a: %{"lat" => lat, "lon" => lon}}) when is_number(lat) and is_number(lon),
    do: {lat * 1.0, lon * 1.0}

  defp link_endpoint(_), do: nil

  defp haversine_km(lat1, lon1, lat2, lon2) do
    r = 6371.0
    phi1 = lat1 * :math.pi() / 180
    phi2 = lat2 * :math.pi() / 180
    dphi = (lat2 - lat1) * :math.pi() / 180
    dlambda = (lon2 - lon1) * :math.pi() / 180

    a =
      :math.sin(dphi / 2) * :math.sin(dphi / 2) +
        :math.cos(phi1) * :math.cos(phi2) * :math.sin(dlambda / 2) * :math.sin(dlambda / 2)

    c = 2 * :math.atan2(:math.sqrt(a), :math.sqrt(1 - a))
    r * c
  end

  defp average([]), do: 0.0
  defp average(list), do: Enum.sum(list) / length(list)

  @spec create_link(map()) :: {:ok, Link.t()} | {:error, Ecto.Changeset.t()}
  def create_link(attrs) do
    %Link{}
    |> Link.changeset(attrs)
    |> Repo.insert()
  end

  @spec create_sample(map()) :: {:ok, Sample.t()} | {:error, Ecto.Changeset.t()}
  def create_sample(attrs) do
    %Sample{}
    |> Sample.changeset(attrs)
    |> Repo.insert()
  end

  @spec weather_stations() :: [String.t()]
  def weather_stations do
    Link
    |> where([l], l.enabled == true and not is_nil(l.weather_station))
    |> select([l], l.weather_station)
    |> distinct(true)
    |> Repo.all()
  end
end
