defmodule Oban.Pro.Workflow.Schema do
  @moduledoc false

  use Ecto.Schema

  import Ecto.Changeset

  @fields ~w(id name parent_id suspended available scheduled executing
             retryable completed cancelled discarded inserted_at meta)a

  @meta_fields ~w(ignore_cancelled ignore_discarded ignore_deleted unique)a

  @states ~w(
      suspended
      available
      scheduled
      executing
      retryable
      completed
      cancelled
      discarded
  )a

  @counts Map.new(@states, &{&1, 0})

  @primary_key {:id, :string, autogenerate: false}
  schema "oban_workflows" do
    field :name, :string
    field :parent_id, :string
    field :state, :string, read_after_writes: true

    field :suspended, :integer, default: 0
    field :available, :integer, default: 0
    field :scheduled, :integer, default: 0
    field :executing, :integer, default: 0
    field :retryable, :integer, default: 0
    field :completed, :integer, default: 0
    field :cancelled, :integer, default: 0
    field :discarded, :integer, default: 0

    field :inserted_at, :utc_datetime_usec
    field :started_at, :utc_datetime_usec
    field :completed_at, :utc_datetime_usec

    field :meta, :map, default: %{}
  end

  @doc false
  def new(params) do
    params = Map.put_new_lazy(params, :inserted_at, &DateTime.utc_now/0)

    %__MODULE__{}
    |> cast(params, @fields)
    |> validate_required(~w(id inserted_at)a)
    |> unique_constraint(:name, name: :oban_workflows_unique_name)
  end

  @doc false
  def from_changesets({workflow_id, changesets}), do: from_changesets(workflow_id, changesets)

  @doc false
  def from_changesets(workflow_id, changesets) do
    [first | _] = changesets
    meta = get_field(first, :meta, %{})

    {queues, workers} =
      Enum.reduce(changesets, {MapSet.new(), MapSet.new()}, fn changeset, {queues, workers} ->
        queue = to_string(get_field(changeset, :queue))
        worker = get_field(changeset, :worker)

        {MapSet.put(queues, queue), MapSet.put(workers, worker)}
      end)

    counts = count_by_state(changesets)

    workflow_meta =
      meta
      |> Map.take(@meta_fields)
      |> Map.put("queues", MapSet.to_list(queues))
      |> Map.put("workers", MapSet.to_list(workers))

    params =
      counts
      |> Map.put(:id, workflow_id)
      |> Map.put(:name, build_name(meta, changesets))
      |> Map.put(:parent_id, meta[:sup_workflow_id])
      |> Map.put(:meta, workflow_meta)

    new(params)
  end

  defp count_by_state(changesets) do
    Enum.reduce(changesets, @counts, fn changeset, acc ->
      state =
        changeset
        |> get_field(:state, "available")
        |> String.to_existing_atom()

      Map.update!(acc, state, &(&1 + 1))
    end)
  end

  defp build_name(%{workflow_name: name}, _) when is_binary(name), do: name

  defp build_name(_meta, changesets) do
    Enum.find_value(changesets, fn changeset ->
      meta = get_field(changeset, :meta)
      worker = get_field(changeset, :worker)

      cond do
        name = meta[:decorated_name] -> name
        worker != "Oban.Pro.Workers.Context" -> worker
        true -> nil
      end
    end)
  end
end
