defmodule Oban.Pro.Queue do
  @moduledoc false

  use Ecto.Schema

  import Ecto.Changeset

  alias Oban.Pro.{Producer, Utils}

  @allowed ~w(ack_async local_limit paused refresh_interval)a

  @primary_key {:name, :string, autogenerate: false}
  schema "oban_queues" do
    field :lock_version, :integer, default: 1
    field :hash, :string

    embeds_one :only, Only, on_replace: :update, primary_key: false do
      @moduledoc false

      field :mode, Ecto.Enum, values: [:node, :sys_env]
      field :op, Ecto.Enum, values: [:==, :!=, :=~]
      field :key, :string
      field :value, :string
    end

    embeds_one :opts, Opts, on_replace: :update, primary_key: false do
      @moduledoc false

      field :ack_async, :boolean
      field :local_limit, :integer
      field :paused, :boolean
      field :refresh_interval, :integer
      field :xact_delay, :integer
      field :xact_retry, :integer

      embeds_one :global_limit, GlobalLimit, on_replace: :update, primary_key: false do
        @moduledoc false

        field :allowed, :integer
        field :burst, :boolean, default: false

        embeds_one :partition, Partition, on_replace: :update, primary_key: false do
          @moduledoc false

          field :fields, {:array, Ecto.Enum}, values: [:args, :meta, :worker]
          field :keys, {:array, :string}
        end
      end

      embeds_one :rate_limit, RateLimit, on_replace: :update, primary_key: false do
        @moduledoc false

        field :allowed, :integer
        field :period, :integer
        field :window_time, :integer, virtual: true, skip_default_validation: true

        embeds_one :partition, Partition, on_replace: :update, primary_key: false do
          @moduledoc false

          field :fields, {:array, Ecto.Enum}, values: [:args, :meta, :worker]
          field :keys, {:array, :string}
        end
      end
    end

    timestamps(inserted_at: :inserted_at, updated_at: :updated_at, type: :utc_datetime_usec)
  end

  def normalize(params) do
    case params do
      [_ | _] -> Map.new(params)
      {name, [_ | _] = opts} -> %{name: name, opts: Map.new(opts)}
      {name, limit} -> %{name: name, opts: %{local_limit: limit}}
    end
  end

  def changeset(params) do
    changeset(%__MODULE__{}, normalize(params))
  end

  def changeset(schema, params) do
    params =
      params
      |> Map.replace_lazy(:name, fn _ -> to_string(params[:name]) end)
      |> extract_only()

    schema
    |> cast(params, [:name])
    |> cast_embed(:only, with: &only_changeset/2)
    |> cast_embed(:opts, required: true, with: &opts_changeset/2)
    |> validate_required([:name])
    |> validate_length(:name, min: 1)
    |> optimistic_lock(:lock_version)
    |> inject_hash(params)
    |> Utils.enforce_keys(params, __MODULE__)
  end

  defp extract_only(params) do
    case params do
      %{opts: %{only: _}} ->
        {only, params} = pop_in(params, [:opts, :only])

        Map.put(params, :only, cast_only(only))

      _ ->
        params
    end
  end

  defp only_changeset(schema, params) do
    schema
    |> cast(params, ~w(mode op key value)a)
    |> validate_required(~w(mode op value)a)
    |> Utils.enforce_keys(params, __MODULE__.Only)
  end

  defp opts_changeset(schema, params) do
    params = Map.new(params, fn {key, val} -> {Utils.maybe_to_atom(key), val} end)

    Producer.meta_changeset(schema, params, @allowed)
  end

  defp inject_hash(changeset, params) do
    hash =
      params
      |> Map.take(~w(only opts)a)
      |> Enum.map_join(&:erlang.phash2/1)
      |> Utils.hash64()

    put_change(changeset, :hash, hash)
  end

  @spec to_keyword_opts(%{opts: map()} | map()) :: Keyword.t()
  def to_keyword_opts(%__MODULE__{name: queue, opts: opts}) do
    opts
    |> Ecto.embedded_dump(:json)
    |> Map.put(:queue, queue)
    |> to_keyword_opts()
  end

  def to_keyword_opts(opts) do
    for {key, val} <- opts, not is_nil(val), do: {Utils.maybe_to_atom(key), val}
  end

  # Helpers

  defp cast_only({:node, value}), do: cast_only({:node, :==, value})
  defp cast_only({:node, op, value}), do: %{mode: :node, op: op, value: to_string(value)}

  defp cast_only({:sys_env, key, value}), do: cast_only({:sys_env, key, :==, value})

  defp cast_only({:sys_env, key, op, value}) do
    %{mode: :sys_env, op: op, key: key, value: to_string(value)}
  end

  defp cast_only(other), do: other
end
