defmodule Microwaveprop.Propagation.NotifyListener do
  @moduledoc """
  Listens for `NOTIFY propagation_ready '<iso valid_time>'` from the Rust
  `prop-grid-rs` worker. When a notification arrives, warms
  `ScoreCache` for every band at that `valid_time` (reading from the
  shared NFS `/data/scores/<band>/<iso>.ntms` tree that Rust just
  wrote) and fans out the standard `"propagation:updated"` PubSub so
  LiveView clients refresh without polling.

  No-op under `testing: :inline` Oban / when the Postgrex.Notifications
  supervisor isn't started — the listener is additive; shadow mode and
  unit tests don't need it running.
  """

  use GenServer

  alias Microwaveprop.Propagation
  alias Microwaveprop.Propagation.BandConfig
  alias Microwaveprop.Propagation.ScoreCache
  alias Microwaveprop.Repo

  require Logger

  @channel "propagation_ready"
  @pubsub Microwaveprop.PubSub
  @topic "propagation:updated"

  @spec start_link(keyword()) :: GenServer.on_start()
  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts, name: __MODULE__)
  end

  @impl true
  def init(_opts) do
    send(self(), :subscribe)
    {:ok, %{ref: nil, pid: nil}}
  end

  @impl true
  def handle_info(:subscribe, state) do
    case start_notifications_conn() do
      {:ok, pid, ref} ->
        {:noreply, %{state | pid: pid, ref: ref}}

      {:error, reason} ->
        Logger.warning("NotifyListener: failed to subscribe to #{@channel}: #{inspect(reason)}")
        # Retry in 30s. An unreachable DB at boot shouldn't wedge the
        # supervisor tree.
        Process.send_after(self(), :subscribe, 30_000)
        {:noreply, state}
    end
  end

  @impl true
  def handle_info({:notification, _pid, _ref, @channel, payload}, state) do
    case DateTime.from_iso8601(payload) do
      {:ok, valid_time, _} ->
        handle_propagation_ready(valid_time)

      _ ->
        Logger.warning("NotifyListener: malformed #{@channel} payload: #{inspect(payload)}")
    end

    {:noreply, state}
  end

  def handle_info(_msg, state), do: {:noreply, state}

  defp start_notifications_conn do
    config = Application.fetch_env!(:microwaveprop, Repo)

    case Postgrex.Notifications.start_link(config) do
      {:ok, pid} ->
        case Postgrex.Notifications.listen(pid, @channel) do
          {:ok, ref} ->
            {:ok, pid, ref}

          other ->
            {:error, other}
        end

      other ->
        other
    end
  end

  defp handle_propagation_ready(valid_time) do
    Enum.each(BandConfig.all_bands(), fn band ->
      Propagation.warm_cache_and_broadcast(band.freq_mhz, valid_time)
    end)

    # Prune anything older than 2 hours so the cache doesn't grow
    # unbounded when Rust covers many forecast hours back-to-back.
    ScoreCache.prune_older_than(DateTime.add(DateTime.utc_now(), -2, :hour))

    Phoenix.PubSub.broadcast(@pubsub, @topic, {:propagation_updated, [valid_time]})

    Logger.info("NotifyListener: warmed ScoreCache + broadcast for valid_time=#{valid_time}")
  end
end
