Skip to main content

lib/mix/tasks/oban_powertools.install.ex

defmodule Mix.Tasks.ObanPowertools.Install do
  use Igniter.Mix.Task

  @shortdoc "Installs Oban Powertools into a Phoenix application"
  @powertools_config_contract """
  config :oban_powertools,
    repo: MyApp.Repo,
    auth_module: MyAppWeb.ObanPowertoolsAuth,
    display_policy: MyAppWeb.ObanPowertoolsDisplayPolicy
  """
  @router_scope_contract """
  scope "/ops/jobs" do
    pipe_through :browser

    require ObanPowertools.Web.Router
    ObanPowertools.Web.Router.oban_powertools_routes("/oban")
  end
  """

  def info(_argv, _composing_task) do
    %Igniter.Mix.Task.Info{
      schema: [],
      positional: []
    }
  end

  def igniter(igniter) do
    igniter
    |> setup_auth_module()
    |> setup_display_policy_module()
    |> setup_runtime_config()
    |> setup_router_scope()
    |> setup_migration()
    |> setup_smart_engine_migrations()
    |> setup_workflow_migrations()
    |> setup_phase_4_migrations()
  end

  defp setup_auth_module(igniter) do
    web_module = Igniter.Libs.Phoenix.web_module(igniter)
    auth_module_name = Module.concat(web_module, "ObanPowertoolsAuth")

    contents = """
      @moduledoc \"\"\"
      Thin host-owned Powertools auth seam.

      Fill in your real operator actor lookup, authorization policy, and durable
      audit principal envelope before exposing operator routes in production.
      \"\"\"
      @behaviour ObanPowertools.Auth

      @impl true
      def current_actor(_conn_or_socket) do
        # TODO: Return the current actor from your session/assigns
        nil
      end

      @impl true
      def authorize(nil, _action, _resource), do: {:error, :unauthorized}

      def authorize(_actor, _action, _resource) do
        # TODO: Authorize Powertools actions for your real operator roles
        {:error, :unauthorized}
      end

      @impl true
      def audit_principal(_actor) do
        # TODO: Return %{id: ..., type: ..., label: ...} for durable audit attribution
        nil
      end
    """

    Igniter.Project.Module.create_module(igniter, auth_module_name, contents)
  end

  defp setup_display_policy_module(igniter) do
    web_module = Igniter.Libs.Phoenix.web_module(igniter)
    display_policy_module_name = Module.concat(web_module, "ObanPowertoolsDisplayPolicy")

    contents = """
      @moduledoc \"\"\"
      Thin host-owned Powertools display policy seam.

      Return redacted or host-formatted values for operator-visible fields.
      \"\"\"

      def display(_kind, _value, _context) do
        # TODO: Redact or format operator-visible values for your host
        nil
      end
    """

    Igniter.Project.Module.create_module(igniter, display_policy_module_name, contents)
  end

  defp setup_runtime_config(igniter) do
    _ = @powertools_config_contract

    app_module = Igniter.Project.Module.module_name_prefix(igniter)
    web_module = Igniter.Libs.Phoenix.web_module(igniter)
    repo_module = Module.concat(app_module, "Repo")
    auth_module_name = Module.concat(web_module, "ObanPowertoolsAuth")
    display_policy_module_name = Module.concat(web_module, "ObanPowertoolsDisplayPolicy")

    igniter
    |> Igniter.Project.Config.configure_new(
      "config.exs",
      :oban_powertools,
      [:repo],
      {:code, quote(do: unquote(repo_module))}
    )
    |> Igniter.Project.Config.configure_new(
      "config.exs",
      :oban_powertools,
      [:auth_module],
      {:code, quote(do: unquote(auth_module_name))}
    )
    |> Igniter.Project.Config.configure_new(
      "config.exs",
      :oban_powertools,
      [:display_policy],
      {:code, quote(do: unquote(display_policy_module_name))}
    )
  end

  defp setup_router_scope(igniter) do
    _ = @router_scope_contract

    router_contents = """
      pipe_through :browser

      require ObanPowertools.Web.Router
      ObanPowertools.Web.Router.oban_powertools_routes("/oban")
    """

    Igniter.Libs.Phoenix.add_scope(
      igniter,
      "/ops/jobs",
      router_contents,
      []
    )
  end

  defp setup_migration(igniter) do
    igniter
    |> Igniter.Libs.Ecto.gen_migration(
      repo_module(igniter),
      "oban_powertools_audit_events",
      timestamp: migration_timestamp(0),
      body: """
        def change do
          create table(:oban_powertools_audit_events) do
            add :actor_id, :string
            add :action, :string, null: false
            add :command_key, :string
            add :event_type, :string
            add :resource, :string
            add :resource_type, :string
            add :resource_id, :string
            add :metadata, :map, default: %{}

            timestamps(updated_at: false)
          end
          
          create index(:oban_powertools_audit_events, [:actor_id])
          create index(:oban_powertools_audit_events, [:action])
          create index(:oban_powertools_audit_events, [:event_type])
          create index(:oban_powertools_audit_events, [:resource_type, :resource_id])
        end
      """
    )
    |> Igniter.Libs.Ecto.gen_migration(
      repo_module(igniter),
      "oban_powertools_idempotency_receipts",
      timestamp: migration_timestamp(1),
      body: """
        def change do
          create table(:oban_powertools_idempotency_receipts, primary_key: false) do
            add :id, :uuid, primary_key: true
            add :worker, :string, null: false
            add :fingerprint, :string, null: false
            add :job_id, :bigint
            add :state, :string, null: false
            add :expires_at, :utc_datetime

            timestamps()
          end

          create unique_index(:oban_powertools_idempotency_receipts, [:worker, :fingerprint])
          create index(:oban_powertools_idempotency_receipts, [:job_id])
        end
      """
    )
  end

  defp setup_smart_engine_migrations(igniter) do
    igniter
    |> Igniter.Libs.Ecto.gen_migration(
      repo_module(igniter),
      "oban_powertools_limit_resources",
      timestamp: migration_timestamp(10),
      body: """
        def change do
          create table(:oban_powertools_limit_resources, primary_key: false) do
            add :id, :uuid, primary_key: true
            add :name, :string, null: false
            add :scope_kind, :string, null: false
            add :algorithm, :string, null: false
            add :bucket_span_ms, :bigint, null: false
            add :bucket_capacity, :integer, null: false
            add :default_weight, :integer, null: false, default: 1
            add :partition_strategy, :string, null: false, default: "global"
            add :partition_config, :map, null: false, default: %{}
            add :cooldown_enabled, :boolean, null: false, default: true
            add :metadata, :map, null: false, default: %{}

            timestamps()
          end

          create unique_index(:oban_powertools_limit_resources, [:name])
          create index(:oban_powertools_limit_resources, [:scope_kind])
          create index(:oban_powertools_limit_resources, [:algorithm])
        end
      """
    )
    |> Igniter.Libs.Ecto.gen_migration(
      repo_module(igniter),
      "oban_powertools_limit_states",
      timestamp: migration_timestamp(11),
      body: """
        def change do
          create table(:oban_powertools_limit_states, primary_key: false) do
            add :id, :uuid, primary_key: true
            add :resource_id, references(:oban_powertools_limit_resources, type: :uuid, on_delete: :delete_all), null: false
            add :partition_key, :string, null: false, default: "__global__"
            add :tokens_used, :integer, null: false, default: 0
            add :bucket_started_at, :utc_datetime_usec, null: false
            add :last_reserved_at, :utc_datetime_usec
            add :cooldown_until, :utc_datetime_usec
            add :cooldown_reason, :string
            add :reservation_snapshot, :map, null: false, default: %{}

            timestamps()
          end

          create unique_index(:oban_powertools_limit_states, [:resource_id, :partition_key])
          create index(:oban_powertools_limit_states, [:cooldown_until])
        end
      """
    )
    |> Igniter.Libs.Ecto.gen_migration(
      repo_module(igniter),
      "oban_powertools_cron_entries",
      timestamp: migration_timestamp(12),
      body: """
        def change do
          create table(:oban_powertools_cron_entries, primary_key: false) do
            add :id, :uuid, primary_key: true
            add :name, :string, null: false
            add :source, :string, null: false
            add :worker, :string, null: false
            add :queue, :string, null: false, default: "default"
            add :expression, :string, null: false
            add :timezone, :string, null: false, default: "Etc/UTC"
            add :args, :map, null: false, default: %{}
            add :opts, :map, null: false, default: %{}
            add :overlap_policy, :string, null: false, default: "queue_one"
            add :catch_up_policy, :string, null: false, default: "latest"
            add :max_catch_up, :integer, null: false, default: 1
            add :paused_at, :utc_datetime_usec
            add :last_run_at, :utc_datetime_usec
            add :metadata, :map, null: false, default: %{}

            timestamps()
          end

          create unique_index(:oban_powertools_cron_entries, [:name])
          create index(:oban_powertools_cron_entries, [:source])
          create index(:oban_powertools_cron_entries, [:paused_at])
        end
      """
    )
    |> Igniter.Libs.Ecto.gen_migration(
      repo_module(igniter),
      "oban_powertools_cron_slots",
      timestamp: migration_timestamp(13),
      body: """
        def change do
          create table(:oban_powertools_cron_slots, primary_key: false) do
            add :id, :uuid, primary_key: true
            add :entry_id, references(:oban_powertools_cron_entries, type: :uuid, on_delete: :delete_all), null: false
            add :slot_at, :utc_datetime_usec, null: false
            add :state, :string, null: false, default: "pending"
            add :job_id, :bigint
            add :claim_token, :uuid
            add :claimed_at, :utc_datetime_usec
            add :finished_at, :utc_datetime_usec
            add :attempt_count, :integer, null: false, default: 0
            add :policy_snapshot, :map, null: false, default: %{}
            add :metadata, :map, null: false, default: %{}

            timestamps()
          end

          create unique_index(:oban_powertools_cron_slots, [:entry_id, :slot_at])
          create index(:oban_powertools_cron_slots, [:state])
          create index(:oban_powertools_cron_slots, [:job_id])
        end
      """
    )
    |> Igniter.Libs.Ecto.gen_migration(
      repo_module(igniter),
      "oban_powertools_blocker_snapshots",
      timestamp: migration_timestamp(14),
      body: """
        def change do
          create table(:oban_powertools_blocker_snapshots, primary_key: false) do
            add :id, :uuid, primary_key: true
            add :job_id, :bigint, null: false
            add :worker, :string, null: false
            add :status, :string, null: false, default: "blocked"
            add :scope_kind, :string, null: false
            add :scope_id, :string, null: false
            add :blocker_codes, {:array, :string}, null: false, default: []
            add :details, :map, null: false, default: %{}
            add :captured_at, :utc_datetime_usec, null: false

            timestamps(updated_at: false)
          end

          create index(:oban_powertools_blocker_snapshots, [:job_id])
          create index(:oban_powertools_blocker_snapshots, [:worker])
          create index(:oban_powertools_blocker_snapshots, [:scope_kind, :scope_id])
        end
      """
    )
    |> Igniter.Libs.Ecto.gen_migration(
      repo_module(igniter),
      "oban_powertools_limiter_history_facts",
      timestamp: migration_timestamp(15),
      body: """
        def change do
          create table(:oban_powertools_limiter_history_facts, primary_key: false) do
            add :id, :uuid, primary_key: true
            add :resource_name, :string, null: false
            add :partition_key, :string, null: false, default: "__global__"
            add :event_type, :string, null: false
            add :cause_kind, :string
            add :occurred_at, :utc_datetime_usec, null: false
            add :eligible_at, :utc_datetime_usec
            add :metadata, :map, null: false, default: %{}

            timestamps(updated_at: false)
          end

          create index(:oban_powertools_limiter_history_facts, [:resource_name, :occurred_at])
          create index(:oban_powertools_limiter_history_facts, [:event_type])
        end
      """
    )
    |> Igniter.Libs.Ecto.gen_migration(
      repo_module(igniter),
      "oban_powertools_cron_coverages",
      timestamp: migration_timestamp(16),
      body: """
        def change do
          create table(:oban_powertools_cron_coverages, primary_key: false) do
            add :id, :uuid, primary_key: true
            add :entry_id, references(:oban_powertools_cron_entries, type: :uuid, on_delete: :delete_all), null: false
            add :slot_at, :utc_datetime_usec, null: false
            add :status, :string, null: false, default: "healthy"
            add :metadata, :map, null: false, default: %{}

            timestamps(updated_at: false)
          end

          create unique_index(:oban_powertools_cron_coverages, [:entry_id, :slot_at])
          create index(:oban_powertools_cron_coverages, [:status])
        end
      """
    )
  end

  defp setup_workflow_migrations(igniter) do
    igniter
    |> Igniter.Libs.Ecto.gen_migration(
      repo_module(igniter),
      "oban_powertools_workflows",
      timestamp: migration_timestamp(20),
      body: """
        def change do
          create table(:oban_powertools_workflows, primary_key: false) do
            add :id, :uuid, primary_key: true
            add :name, :string, null: false
            add :state, :string, null: false, default: "pending"
            add :workflow_context, :map, null: false, default: %{}
            add :definition_version, :integer, null: false, default: 1
            add :semantics_version, :integer, null: false, default: 2
            add :step_count, :integer, null: false, default: 0
            add :runnable_step_count, :integer, null: false, default: 0
            add :completed_step_count, :integer, null: false, default: 0
            add :cancelled_step_count, :integer, null: false, default: 0
            add :failed_step_count, :integer, null: false, default: 0
            add :terminal_cause, :string
            add :cancel_requested_at, :utc_datetime_usec
            add :last_transition_at, :utc_datetime_usec
            add :started_at, :utc_datetime_usec
            add :finished_at, :utc_datetime_usec
            add :cancelled_at, :utc_datetime_usec

            timestamps()
          end

          create unique_index(:oban_powertools_workflows, [:name])
          create index(:oban_powertools_workflows, [:state])
        end
      """
    )
    |> Igniter.Libs.Ecto.gen_migration(
      repo_module(igniter),
      "oban_powertools_workflow_steps",
      timestamp: migration_timestamp(21),
      body: """
        def change do
          create table(:oban_powertools_workflow_steps, primary_key: false) do
            add :id, :uuid, primary_key: true
            add :workflow_id, references(:oban_powertools_workflows, type: :uuid, on_delete: :delete_all), null: false
            add :step_name, :string, null: false
            add :worker, :string, null: false
            add :input, :map, null: false, default: %{}
            add :context, :map, null: false, default: %{}
            add :state, :string, null: false, default: "pending"
            add :job_id, :bigint
            add :queue, :string, null: false, default: "default"
            add :attempt, :integer, null: false, default: 0
            add :position, :integer, null: false, default: 0
            add :dependency_count, :integer, null: false, default: 0
            add :dependency_snapshot, :map, null: false, default: %{}
            add :blocker_codes, {:array, :string}, null: false, default: []
            add :blocker_details, :map, null: false, default: %{}
            add :terminal_cause, :string
            add :active_await_id, :uuid
            add :awaiting_signal_name, :string
            add :await_correlation_key, :string
            add :await_dedupe_key, :string
            add :await_deadline_at, :utc_datetime_usec
            add :cancel_requested_at, :utc_datetime_usec
            add :last_transition_at, :utc_datetime_usec
            add :nested_workflow_id, references(:oban_powertools_workflows, type: :uuid, on_delete: :nilify_all)
            add :started_at, :utc_datetime_usec
            add :finished_at, :utc_datetime_usec
            add :cancelled_at, :utc_datetime_usec

            timestamps()
          end

          create unique_index(:oban_powertools_workflow_steps, [:workflow_id, :step_name])
          create index(:oban_powertools_workflow_steps, [:state])
          create index(:oban_powertools_workflow_steps, [:job_id])
          create index(:oban_powertools_workflow_steps, [:nested_workflow_id])
        end
      """
    )
    |> Igniter.Libs.Ecto.gen_migration(
      repo_module(igniter),
      "oban_powertools_workflow_edges",
      timestamp: migration_timestamp(22),
      body: """
        def change do
          create table(:oban_powertools_workflow_edges, primary_key: false) do
            add :id, :uuid, primary_key: true
            add :workflow_id, references(:oban_powertools_workflows, type: :uuid, on_delete: :delete_all), null: false
            add :from_step_id, references(:oban_powertools_workflow_steps, type: :uuid, on_delete: :delete_all), null: false
            add :to_step_id, references(:oban_powertools_workflow_steps, type: :uuid, on_delete: :delete_all), null: false
            add :policy, :string, null: false, default: "cancel"
            add :terminal_snapshot, :map, null: false, default: %{}

            timestamps()
          end

          create unique_index(:oban_powertools_workflow_edges, [:workflow_id, :from_step_id, :to_step_id])
          create index(:oban_powertools_workflow_edges, [:to_step_id])
          create index(:oban_powertools_workflow_edges, [:from_step_id])
          create index(:oban_powertools_workflow_edges, [:policy])
        end
      """
    )
    |> Igniter.Libs.Ecto.gen_migration(
      repo_module(igniter),
      "oban_powertools_workflow_results",
      timestamp: migration_timestamp(23),
      body: """
        def change do
          create table(:oban_powertools_workflow_results, primary_key: false) do
            add :id, :uuid, primary_key: true
            add :workflow_id, references(:oban_powertools_workflows, type: :uuid, on_delete: :delete_all), null: false
            add :step_id, references(:oban_powertools_workflow_steps, type: :uuid, on_delete: :delete_all), null: false
            add :attempt, :integer, null: false, default: 1
            add :status, :string, null: false, default: "ok"
            add :payload, :map, null: false, default: %{}
            add :payload_bytes, :integer, null: false, default: 0
            add :retention, :string, null: false, default: "standard"
            add :redacted, :boolean, null: false, default: false
            add :summary, :string
            add :recorded_at, :utc_datetime_usec, null: false
            add :expires_at, :utc_datetime_usec

            timestamps(updated_at: false)
          end

          create unique_index(:oban_powertools_workflow_results, [:step_id, :attempt])
          create index(:oban_powertools_workflow_results, [:workflow_id])
          create index(:oban_powertools_workflow_results, [:status])
          create index(:oban_powertools_workflow_results, [:expires_at])
        end
      """
    )
    |> Igniter.Libs.Ecto.gen_migration(
      repo_module(igniter),
      "oban_powertools_workflow_semantics",
      timestamp: migration_timestamp(24),
      body: """
        def change do
          create table(:oban_powertools_workflow_awaits, primary_key: false) do
            add :id, :uuid, primary_key: true
            add :workflow_id, references(:oban_powertools_workflows, type: :uuid, on_delete: :delete_all), null: false
            add :step_id, references(:oban_powertools_workflow_steps, type: :uuid, on_delete: :delete_all), null: false
            add :signal_name, :string, null: false
            add :correlation_key, :string, null: false
            add :dedupe_key, :string, null: false
            add :status, :string, null: false, default: "waiting"
            add :resolution_policy, :string, null: false, default: "ignore_late"
            add :deadline_at, :utc_datetime_usec
            add :resolved_at, :utc_datetime_usec
            add :resolved_signal_id, :uuid

            timestamps(updated_at: false)
          end

          create index(:oban_powertools_workflow_awaits, [:workflow_id])
          create index(:oban_powertools_workflow_awaits, [:signal_name, :correlation_key])
          create unique_index(:oban_powertools_workflow_awaits, [:step_id, :status], name: :oban_powertools_workflow_awaits_step_id_status_index)

          create table(:oban_powertools_workflow_signals, primary_key: false) do
            add :id, :uuid, primary_key: true
            add :workflow_id, references(:oban_powertools_workflows, type: :uuid, on_delete: :nilify_all)
            add :matched_step_id, references(:oban_powertools_workflow_steps, type: :uuid, on_delete: :nilify_all)
            add :await_id, references(:oban_powertools_workflow_awaits, type: :uuid, on_delete: :nilify_all)
            add :signal_name, :string, null: false
            add :correlation_key, :string, null: false
            add :dedupe_key, :string, null: false
            add :status, :string, null: false, default: "recorded"
            add :payload, :map, null: false, default: %{}
            add :received_at, :utc_datetime_usec, null: false

            timestamps(updated_at: false)
          end

          create unique_index(:oban_powertools_workflow_signals, [:signal_name, :correlation_key, :dedupe_key], name: :oban_powertools_workflow_signals_dedupe_index)
          create index(:oban_powertools_workflow_signals, [:workflow_id])
          create index(:oban_powertools_workflow_signals, [:status])
          create index(:oban_powertools_workflow_signals, [:signal_name, :correlation_key])

          create table(:oban_powertools_workflow_recovery_sessions, primary_key: false) do
            add :id, :uuid, primary_key: true
            add :workflow_id, references(:oban_powertools_workflows, type: :uuid, on_delete: :delete_all), null: false
            add :status, :string, null: false, default: "completed"
            add :trigger, :string, null: false, default: "recover_step"
            add :reason, :string
            add :actor_id, :string
            add :requested_at, :utc_datetime_usec, null: false
            add :completed_at, :utc_datetime_usec
            add :metadata, :map, null: false, default: %{}

            timestamps(updated_at: false)
          end

          create index(:oban_powertools_workflow_recovery_sessions, [:workflow_id])
          create index(:oban_powertools_workflow_recovery_sessions, [:status])
          create index(:oban_powertools_workflow_recovery_sessions, [:requested_at])

          create table(:oban_powertools_workflow_recovery_attempts, primary_key: false) do
            add :id, :uuid, primary_key: true
            add :workflow_id, references(:oban_powertools_workflows, type: :uuid, on_delete: :delete_all), null: false
            add :step_id, references(:oban_powertools_workflow_steps, type: :uuid, on_delete: :nilify_all)
            add :scope, :string, null: false, default: "step"
            add :action, :string, null: false
            add :status, :string, null: false, default: "requested"
            add :reason, :string
            add :actor_id, :string
            add :requested_at, :utc_datetime_usec, null: false
            add :completed_at, :utc_datetime_usec
            add :before_snapshot, :map, null: false, default: %{}
            add :after_snapshot, :map, null: false, default: %{}
            add :metadata, :map, null: false, default: %{}
            add :recovery_session_id, references(:oban_powertools_workflow_recovery_sessions, type: :uuid, on_delete: :delete_all)

            timestamps(updated_at: false)
          end

          create index(:oban_powertools_workflow_recovery_attempts, [:workflow_id])
          create index(:oban_powertools_workflow_recovery_attempts, [:step_id])
          create index(:oban_powertools_workflow_recovery_attempts, [:status])
          create index(:oban_powertools_workflow_recovery_attempts, [:recovery_session_id])

          create table(:oban_powertools_workflow_callback_outbox, primary_key: false) do
            add :id, :uuid, primary_key: true
            add :workflow_id, references(:oban_powertools_workflows, type: :uuid, on_delete: :delete_all), null: false
            add :recovery_attempt_id, references(:oban_powertools_workflow_recovery_attempts, type: :uuid, on_delete: :nilify_all)
            add :event, :string, null: false
            add :dedupe_key, :string, null: false
            add :status, :string, null: false, default: "pending"
            add :payload, :map, null: false, default: %{}
            add :attempts, :integer, null: false, default: 0
            add :available_at, :utc_datetime_usec
            add :claimed_at, :utc_datetime_usec
            add :claimed_by, :string
            add :lease_expires_at, :utc_datetime_usec
            add :delivered_at, :utc_datetime_usec
            add :last_error, :string

            timestamps()
          end

          create unique_index(:oban_powertools_workflow_callback_outbox, [:dedupe_key])
          create index(:oban_powertools_workflow_callback_outbox, [:workflow_id])
          create index(:oban_powertools_workflow_callback_outbox, [:status, :available_at])
          create index(:oban_powertools_workflow_callback_outbox, [:status, :lease_expires_at])
          create index(:oban_powertools_workflow_callback_outbox, [:claimed_by])
        end
      """
    )
    |> Igniter.Libs.Ecto.gen_migration(
      repo_module(igniter),
      "oban_powertools_workflow_command_attempts",
      timestamp: migration_timestamp(25),
      body: """
        def change do
          create table(:oban_powertools_workflow_command_attempts, primary_key: false) do
            add :id, :uuid, primary_key: true
            add :workflow_id, references(:oban_powertools_workflows, type: :uuid, on_delete: :delete_all)
            add :step_id, references(:oban_powertools_workflow_steps, type: :uuid, on_delete: :nilify_all)
            add :signal_record_id, references(:oban_powertools_workflow_signals, type: :uuid, on_delete: :nilify_all)
            add :scope, :string, null: false, default: "workflow"
            add :action, :string, null: false
            add :status, :string, null: false, default: "completed"
            add :reason_code, :string
            add :reason_message, :string
            add :actor_id, :string
            add :source, :string, null: false, default: "runtime"
            add :requested_at, :utc_datetime_usec, null: false
            add :completed_at, :utc_datetime_usec
            add :before_snapshot, :map, null: false, default: %{}
            add :after_snapshot, :map, null: false, default: %{}
            add :metadata, :map, null: false, default: %{}

            timestamps(updated_at: false)
          end

          create index(:oban_powertools_workflow_command_attempts, [:workflow_id])
          create index(:oban_powertools_workflow_command_attempts, [:step_id])
          create index(:oban_powertools_workflow_command_attempts, [:signal_record_id])
          create index(:oban_powertools_workflow_command_attempts, [:scope, :action])
          create index(:oban_powertools_workflow_command_attempts, [:status])
          create index(:oban_powertools_workflow_command_attempts, [:reason_code])
          create index(:oban_powertools_workflow_command_attempts, [:requested_at])
        end
      """
    )
  end

  defp setup_phase_4_migrations(igniter) do
    igniter
    |> Igniter.Libs.Ecto.gen_migration(
      repo_module(igniter),
      "oban_powertools_heartbeats",
      timestamp: migration_timestamp(30),
      body: """
        def change do
          create table(:oban_powertools_heartbeats, primary_key: false) do
            add :id, :uuid, primary_key: true
            add :executor_id, :string, null: false
            add :oban_name, :string, null: false, default: "Oban"
            add :node, :string, null: false
            add :queue, :string, null: false, default: "default"
            add :producer_scope, :string, null: false
            add :health_state, :string, null: false, default: "healthy"
            add :last_heartbeat_at, :utc_datetime_usec, null: false
            add :warning_threshold_ms, :bigint, null: false, default: 45000
            add :missing_threshold_ms, :bigint, null: false, default: 120000
            add :metadata, :map, null: false, default: %{}

            timestamps()
          end

          create unique_index(:oban_powertools_heartbeats, [:executor_id])
          create index(:oban_powertools_heartbeats, [:health_state])
          create index(:oban_powertools_heartbeats, [:last_heartbeat_at])
        end
      """
    )
    |> Igniter.Libs.Ecto.gen_migration(
      repo_module(igniter),
      "oban_powertools_lifeline_incidents",
      timestamp: migration_timestamp(31),
      body: """
        def change do
          create table(:oban_powertools_lifeline_incidents, primary_key: false) do
            add :id, :uuid, primary_key: true
            add :incident_class, :string, null: false
            add :status, :string, null: false, default: "active"
            add :executor_id, :string
            add :workflow_id, :uuid
            add :workflow_step_id, :uuid
            add :incident_fingerprint, :string, null: false
            add :health_state, :string
            add :summary, :string
            add :affected_counts, :map, null: false, default: %{}
            add :evidence, :map, null: false, default: %{}
            add :first_detected_at, :utc_datetime_usec, null: false
            add :last_detected_at, :utc_datetime_usec, null: false
            add :resolved_at, :utc_datetime_usec
            add :metadata, :map, null: false, default: %{}

            timestamps()
          end

          create unique_index(:oban_powertools_lifeline_incidents, [:incident_fingerprint])
          create index(:oban_powertools_lifeline_incidents, [:incident_class])
          create index(:oban_powertools_lifeline_incidents, [:status])
          create index(:oban_powertools_lifeline_incidents, [:health_state])
        end
      """
    )
    |> Igniter.Libs.Ecto.gen_migration(
      repo_module(igniter),
      "oban_powertools_repair_previews",
      timestamp: migration_timestamp(32),
      body: """
        def change do
          create table(:oban_powertools_repair_previews, primary_key: false) do
            add :id, :uuid, primary_key: true
            add :incident_id, :uuid
            add :incident_class, :string, null: false
            add :incident_fingerprint, :string, null: false
            add :plan_hash, :string, null: false
            add :preview_token, :uuid, null: false
            add :action, :string, null: false
            add :target_type, :string, null: false
            add :target_id, :string, null: false
            add :health_state, :string
            add :status, :string, null: false, default: "pending"
            add :affected_counts, :map, null: false, default: %{}
            add :before_snapshot, :map, null: false, default: %{}
            add :after_snapshot, :map, null: false, default: %{}
            add :evidence, :map, null: false, default: %{}
            add :reason_required, :boolean, null: false, default: true
            add :executed_at, :utc_datetime_usec
            add :consumed_at, :utc_datetime_usec
            add :expires_at, :utc_datetime_usec
            add :metadata, :map, null: false, default: %{}

            timestamps()
          end

          create unique_index(:oban_powertools_repair_previews, [:preview_token])
          create index(:oban_powertools_repair_previews, [:incident_class])
          create index(:oban_powertools_repair_previews, [:status])
          create index(:oban_powertools_repair_previews, [:incident_fingerprint])
          create index(:oban_powertools_repair_previews, [:plan_hash])
        end
      """
    )
    |> Igniter.Libs.Ecto.gen_migration(
      repo_module(igniter),
      "oban_powertools_archive_runs",
      timestamp: migration_timestamp(33),
      body: """
        def change do
          create table(:oban_powertools_archive_runs, primary_key: false) do
            add :id, :uuid, primary_key: true
            add :run_type, :string, null: false
            add :status, :string, null: false, default: "pending"
            add :retention_class, :string, null: false
            add :actor_id, :string
            add :reason, :string
            add :batch_size, :integer, null: false, default: 100
            add :archived_count, :integer, null: false, default: 0
            add :pruned_count, :integer, null: false, default: 0
            add :blocked_count, :integer, null: false, default: 0
            add :started_at, :utc_datetime_usec
            add :finished_at, :utc_datetime_usec
            add :metadata, :map, null: false, default: %{}

            timestamps()
          end

          create index(:oban_powertools_archive_runs, [:run_type])
          create index(:oban_powertools_archive_runs, [:status])
          create index(:oban_powertools_archive_runs, [:retention_class])
          create index(:oban_powertools_archive_runs, [:started_at])
        end
      """
    )
    |> Igniter.Libs.Ecto.gen_migration(
      repo_module(igniter),
      "oban_powertools_repair_archives",
      timestamp: migration_timestamp(34),
      body: """
        def change do
          create table(:oban_powertools_repair_archives, primary_key: false) do
            add :id, :uuid, primary_key: true
            add :archive_run_id, references(:oban_powertools_archive_runs, type: :uuid, on_delete: :nilify_all)
            add :audit_event_id, references(:oban_powertools_audit_events, on_delete: :nilify_all)
            add :resource_type, :string, null: false
            add :resource_id, :string, null: false
            add :action, :string, null: false
            add :incident_class, :string
            add :incident_fingerprint, :string
            add :plan_hash, :string
            add :reason, :string
            add :actor_id, :string
            add :affected_counts, :map, null: false, default: %{}
            add :evidence, :map, null: false, default: %{}
            add :archived_at, :utc_datetime_usec, null: false
            add :metadata, :map, null: false, default: %{}

            timestamps(updated_at: false)
          end

          create index(:oban_powertools_repair_archives, [:archive_run_id])
          create index(:oban_powertools_repair_archives, [:audit_event_id])
          create index(:oban_powertools_repair_archives, [:resource_type, :resource_id])
          create index(:oban_powertools_repair_archives, [:incident_class])
          create index(:oban_powertools_repair_archives, [:archived_at])
        end
      """
    )
  end

  defp repo_module(igniter) do
    Module.concat(Igniter.Project.Module.module_name_prefix(igniter), "Repo")
  end

  defp migration_timestamp(offset_seconds) do
    DateTime.utc_now()
    |> DateTime.add(offset_seconds, :second)
    |> Calendar.strftime("%Y%m%d%H%M%S")
  end
end