defmodule Microwaveprop.Workers.ContactImportWorker do
  @moduledoc """
  Processes one chunk of rows out of an `ImportRun` — the async half of
  the CSV import flow.

  Each job receives an `import_run_id`, an `offset`, and a `limit`. The
  worker loads the run, takes that slice of rows, routes them through
  `CsvImport.commit_rows/1`, atomically bumps the per-run counters, and
  broadcasts the updated run on `"csv_import:<id>"` so the submitting
  LiveView can reflect progress.

  Counters are updated with a single `UPDATE ... SET col = col + ?`
  query so concurrent workers finishing at the same time can't clobber
  each other. The worker that pushes `processed_rows` past `total_rows`
  flips `status` to `"complete"` and sets `completed_at`; any worker
  that sees `status = 'pending'` on its first increment flips it to
  `"running"` and sets `started_at`.
  """
  use Oban.Worker, queue: :contact_import, max_attempts: 3

  import Ecto.Query

  alias Microwaveprop.Radio.CsvImport
  alias Microwaveprop.Radio.ImportRun
  alias Microwaveprop.Repo

  require Logger

  @pubsub Microwaveprop.PubSub

  @impl Oban.Worker
  def perform(%Oban.Job{args: %{"import_run_id" => run_id, "offset" => offset, "limit" => limit}}) do
    case Repo.get(ImportRun, run_id) do
      nil ->
        Logger.warning("ContactImportWorker: import_run #{run_id} not found; discarding chunk")
        :ok

      %ImportRun{} = run ->
        process_chunk(run, offset, limit)
    end
  end

  defp process_chunk(run, offset, limit) do
    all_rows = get_in(run.rows, ["rows"]) || []
    chunk = Enum.slice(all_rows, offset, limit)

    commit_input = build_commit_input(chunk)
    commit_result = CsvImport.commit_rows(commit_input)

    updates = build_counter_updates(commit_result, length(chunk))
    updated_run = apply_chunk_updates(run.id, updates)

    broadcast(run.id, updated_run)

    :ok
  end

  defp build_commit_input(chunk) do
    {valid, refinements} =
      Enum.reduce(chunk, {[], []}, fn row, {valid, refinements} ->
        case CsvImport.decode_row(row) do
          {:valid, decoded} -> {[decoded | valid], refinements}
          {:refinement, decoded} -> {valid, [decoded | refinements]}
        end
      end)

    %{valid: Enum.reverse(valid), refinements: Enum.reverse(refinements)}
  end

  defp build_counter_updates(commit_result, chunk_len) do
    errors_map =
      Map.new(commit_result.errors, fn {row_num, messages} ->
        {to_string(row_num), messages}
      end)

    %{
      chunk_len: chunk_len,
      imported_count: length(commit_result.imported),
      refined_count: length(commit_result.refined),
      error_count: length(commit_result.errors),
      errors_map: errors_map
    }
  end

  # Atomic per-chunk update. All counter fields are incremented in one
  # UPDATE so concurrent chunk-finishers can't clobber each other, and
  # the status / timestamp transitions are expressed as CASEs off the
  # pre-increment values.
  defp apply_chunk_updates(run_id, u) do
    now = DateTime.utc_now()
    chunk_len = u.chunk_len
    imported_count = u.imported_count
    refined_count = u.refined_count
    error_count = u.error_count
    errors_map = u.errors_map

    query =
      from(r in ImportRun,
        where: r.id == ^run_id,
        update: [
          set: [
            updated_at: ^now,
            processed_rows: fragment("? + ?", r.processed_rows, ^chunk_len),
            imported_count: fragment("? + ?", r.imported_count, ^imported_count),
            refined_count: fragment("? + ?", r.refined_count, ^refined_count),
            error_count: fragment("? + ?", r.error_count, ^error_count),
            status:
              fragment(
                """
                CASE
                  WHEN ? + ? >= ? THEN 'complete'
                  WHEN ? = 'pending' THEN 'running'
                  ELSE ?
                END
                """,
                r.processed_rows,
                ^chunk_len,
                r.total_rows,
                r.status,
                r.status
              ),
            started_at:
              fragment(
                "CASE WHEN ? IS NULL THEN ? ELSE ? END",
                r.started_at,
                ^now,
                r.started_at
              ),
            completed_at:
              fragment(
                "CASE WHEN ? + ? >= ? THEN ? ELSE ? END",
                r.processed_rows,
                ^chunk_len,
                r.total_rows,
                ^now,
                r.completed_at
              ),
            errors: fragment("? || ?::jsonb", r.errors, ^errors_map)
          ]
        ]
      )

    {1, _} = Repo.update_all(query, [])

    Repo.get!(ImportRun, run_id)
  end

  defp broadcast(run_id, run) do
    Phoenix.PubSub.broadcast(@pubsub, "csv_import:#{run_id}", {:import_progress, run_id, run})
  end
end
