defmodule Oban.Pro.Workers.Workflow do
  @moduledoc """
  A dedicated worker for linking the execution of jobs.

  This worker is deprecated in favor of `Oban.Pro.Workflow`, which is more flexible and supports
  more options.
  """

  @moduledoc deprecated: "Use Oban.Pro.Workflow instead"

  import Ecto.Query, only: [group_by: 3, select: 3, where: 3]

  alias Oban.{Job, Repo}
  alias Oban.Pro.Workflow

  @callback new_workflow(Workflow.new_opts()) :: Workflow.t()

  @callback add(Workflow.t(), Workflow.name(), Job.changeset(), Workflow.add_opts()) ::
              Workflow.t()

  @callback after_cancelled(Workflow.cancel_reason(), Job.t()) :: :ok

  @callback all_workflow_jobs(Job.t(), Workflow.fetch_opts()) :: [Job.t()]

  @callback append_workflow(Job.t() | [Job.t()], Workflow.append_opts()) :: Workflow.t()

  @callback gen_id() :: String.t()

  @callback stream_workflow_jobs(Job.t(), Workflow.fetch_opts()) :: Enum.t()

  @callback to_dot(Workflow.t()) :: String.t()

  @optional_callbacks after_cancelled: 2

  defmacro __using__(opts) do
    quote do
      use Oban.Pro.Worker, unquote(opts)

      @behaviour Oban.Pro.Workers.Workflow

      @impl Oban.Pro.Workers.Workflow
      def new_workflow(opts \\ []) when is_list(opts) do
        opts
        |> Keyword.put_new(:workflow_id, gen_id())
        |> Oban.Pro.Workflow.new()
      end

      @impl Oban.Pro.Workers.Workflow
      def append_workflow(jobs, opts \\ []) do
        Oban.Pro.Workflow.append(jobs, opts)
      end

      @impl Oban.Pro.Workers.Workflow
      def add(workflow, name, changeset, opts \\ []) do
        Oban.Pro.Workflow.add(workflow, name, changeset, opts)
      end

      @impl Oban.Pro.Workers.Workflow
      def gen_id, do: Oban.Pro.UUIDv7.generate()

      @impl Oban.Pro.Workers.Workflow
      def to_dot(workflow) do
        Oban.Pro.Workflow.to_dot(workflow)
      end

      @impl Oban.Pro.Workers.Workflow
      def stream_workflow_jobs(%Job{} = job, opts \\ []) do
        Oban.Pro.Workflow.stream_jobs(Oban, job, opts)
      end

      @impl Oban.Pro.Workers.Workflow
      def all_workflow_jobs(%Job{} = job, opts \\ []) do
        Oban.Pro.Workflow.all_jobs(Oban, job, opts)
      end

      @impl Oban.Worker
      def perform(%Job{} = job) do
        opts = __opts__()

        with {:ok, job} <- Oban.Pro.Worker.before_process(__MODULE__, job, opts) do
          job
          |> Oban.Pro.Workers.Workflow.maybe_process(__MODULE__)
          |> Oban.Pro.Worker.after_process(__MODULE__, job, opts)
        end
      end

      defoverridable Oban.Pro.Workers.Workflow
    end
  end

  @doc false
  defdelegate add(workflow, name, changeset, opts \\ []), to: Workflow

  @doc false
  def all_jobs(job, opts \\ []), do: Workflow.all_jobs(Oban, job, opts)

  @doc false
  defdelegate append(job, opts \\ []), to: Workflow

  @doc false
  def gen_id, do: Oban.Pro.UUIDv7.generate()

  @doc false
  defdelegate new(opts \\ []), to: Workflow

  @doc false
  def stream_jobs(job, opts \\ []), do: Workflow.stream_jobs(Oban, job, opts)

  @doc false
  defdelegate to_dot(workflow), to: Workflow

  @doc false
  def maybe_process(%{meta: meta} = job, module) do
    if is_map_key(meta, "workflow_id") and not is_map_key(meta, "on_hold") do
      legacy_process(job, module)
    else
      module.process(job)
    end
  end

  @legacy_meta %{
    ignore_cancelled: false,
    ignore_deleted: false,
    ignore_discarded: false,
    waiting_delay: :timer.seconds(1),
    waiting_limit: 10,
    waiting_snooze: 5
  }

  # This is necessary for backward compatibility
  defp legacy_process(job, module, waiting_count \\ 0) do
    meta = for {key, val} <- @legacy_meta, into: %{}, do: {key, job.meta[to_string(key)] || val}

    case legacy_check_deps(job) do
      :completed ->
        module.process(job)

      :available ->
        {:snooze, meta.waiting_snooze}

      :executing ->
        if waiting_count >= meta.waiting_limit do
          {:snooze, meta.waiting_snooze}
        else
          Process.sleep(meta.waiting_delay)

          legacy_process(job, module, waiting_count + 1)
        end

      {:scheduled, scheduled_at} ->
        seconds =
          scheduled_at
          |> DateTime.diff(DateTime.utc_now())
          |> max(meta.waiting_snooze)

        {:snooze, seconds}

      :cancelled ->
        if meta.ignore_cancelled do
          module.process(job)
        else
          {:cancel, "upstream deps cancelled, workflow will never complete"}
        end

      :discarded ->
        if meta.ignore_discarded do
          module.process(job)
        else
          {:cancel, "upstream deps discarded, workflow will never complete"}
        end

      :deleted ->
        if meta.ignore_deleted do
          module.process(job)
        else
          {:cancel, "upstream deps deleted, workflow will never complete"}
        end
    end
  end

  defp legacy_check_deps(%{conf: conf, meta: %{"deps" => [_ | _]}} = job) do
    %{"deps" => deps, "workflow_id" => workflow_id} = job.meta

    deps_count = length(deps)

    query =
      Job
      |> where([j], fragment("? @> ?", j.meta, ^%{workflow_id: workflow_id}))
      |> where([j], fragment("?->>'name'", j.meta) in ^deps)
      |> group_by([j], j.state)
      |> select([j], {j.state, count(j.id), max(j.scheduled_at)})

    conf
    |> Repo.all(query)
    |> Map.new(fn {state, count, sc_at} -> {state, {count, sc_at}} end)
    |> case do
      %{"completed" => {^deps_count, _}} -> :completed
      %{"scheduled" => {_, scheduled_at}} -> {:scheduled, scheduled_at}
      %{"retryable" => {_, scheduled_at}} -> {:scheduled, scheduled_at}
      %{"executing" => _} -> :executing
      %{"available" => _} -> :available
      %{"cancelled" => _} -> :cancelled
      %{"discarded" => _} -> :discarded
      %{} -> :deleted
    end
  end

  defp legacy_check_deps(_job), do: :completed
end
