defmodule Microwaveprop.Propagation.GridTaskEnqueuer do @moduledoc """ Seeds `grid_tasks` rows for the Rust `prop-grid-rs` worker. Called from `PropagationGridWorker.seed_chain/0`. Rust claims kind='forecast' and kind='analysis' rows, with analysis lanes taking priority (see `claim_next_analysis` in the Rust db module). Inserts are idempotent via the `(run_time, forecast_hour, kind)` unique index — re-seeding the same cycle is a no-op. """ alias Microwaveprop.Repo require Logger @max_forecast_hour 18 @doc """ Seed one kind='analysis' row (f00) plus 18 kind='forecast' rows (f01..f18) for `run_time`. This is the Phase 3 Stream A cutover shape: Rust owns the entire chain end-to-end. """ @spec seed_with_analysis(DateTime.t()) :: {:ok, non_neg_integer()} | {:error, term()} def seed_with_analysis(%DateTime{} = run_time) do run_time = DateTime.truncate(run_time, :second) now = DateTime.truncate(DateTime.utc_now(), :microsecond) analysis_row = %{ id: Ecto.UUID.bingenerate(), run_time: run_time, forecast_hour: 0, valid_time: run_time, status: "queued", attempt: 0, kind: "analysis", claimed_at: nil, completed_at: nil, error: nil, inserted_at: now, updated_at: now } forecast_rows = for fh <- 1..@max_forecast_hour do %{ id: Ecto.UUID.bingenerate(), run_time: run_time, forecast_hour: fh, valid_time: DateTime.add(run_time, fh * 3600, :second), status: "queued", attempt: 0, kind: "forecast", claimed_at: nil, completed_at: nil, error: nil, inserted_at: now, updated_at: now } end do_insert([analysis_row | forecast_rows], run_time) end @doc """ Legacy: seed only kind='forecast' rows (f01..f18). Retained for tooling that needs to re-seed the forecast lane without touching the analysis row. `seed_with_analysis/1` is the production path. """ @spec seed(DateTime.t()) :: {:ok, non_neg_integer()} | {:error, term()} def seed(%DateTime{} = run_time) do run_time = DateTime.truncate(run_time, :second) now = DateTime.truncate(DateTime.utc_now(), :microsecond) rows = for fh <- 1..@max_forecast_hour do %{ id: Ecto.UUID.bingenerate(), run_time: run_time, forecast_hour: fh, valid_time: DateTime.add(run_time, fh * 3600, :second), status: "queued", attempt: 0, kind: "forecast", claimed_at: nil, completed_at: nil, error: nil, inserted_at: now, updated_at: now } end do_insert(rows, run_time) end defp do_insert(rows, run_time) do {count, _} = Repo.insert_all("grid_tasks", rows, on_conflict: :nothing, conflict_target: [:run_time, :forecast_hour, :kind] ) if count > 0 do Logger.info("GridTaskEnqueuer: seeded #{count} grid_tasks for run_time=#{run_time}") else Logger.info("GridTaskEnqueuer: run_time=#{run_time} already seeded") end {:ok, count} rescue e -> Logger.error("GridTaskEnqueuer: seed failed: #{inspect(e)}") {:error, e} end end