defmodule Bonny.Pluggable.SkipObservedGenerations do
  @moduledoc """
  Halts the pipelines for a defined list of actions if the observed generation
  equals the resource's generation. It also sets the observed generation value
  before applying the resource status to the cluster.

  ## Options

  * `:actions` - The actions for which this rule applies. Defaults to `[:add, :modify]`.
  * `:observed_generation_key` - The resource status key where the observed generation is stored. This will be passed to `Kernel.get_in()`. Defaults to `["status", "observedGeneration"]`.

  ## Usage

      step Bonny.Pluggable.SkipObservedGenerations,
        actions: [:add, :modify, :reconcile],
        observed_generation_key: ~w(status internal observedGeneration)
  """

  @behaviour Pluggable
  import Pluggable.Token
  alias Bonny.Axn

  @impl true
  def init(opts \\ []) do
    observed_generation_key =
      Keyword.get(opts, :observed_generation_key, ["status", "observedGeneration"])

    opts
    |> Keyword.put_new(:actions, ~w(add modify)a)
    |> Keyword.put(:observed_generation_key, observed_generation_key)
  end

  @impl true
  def call(%Axn{resource: resource} = axn, opts) do
    actions = Keyword.fetch!(opts, :actions)

    observed_generation_key = Keyword.fetch!(opts, :observed_generation_key)

    cond do
      axn.action not in actions ->
        set_observed_generation(axn, observed_generation_key)

      get_in(resource, ~w(metadata generation)) !=
          get_in(resource, observed_generation_key) ->
        set_observed_generation(axn, observed_generation_key)

      true ->
        halt(axn)
    end
  end

  defp set_observed_generation(axn, ["status" | rest]), do: set_observed_generation(axn, rest)

  defp set_observed_generation(axn, observed_generation_key) do
    case axn.resource["metadata"]["generation"] do
      nil ->
        axn

      generation ->
        accessible_observed_generation_key =
          accessible_observed_generation_key(observed_generation_key)

        Axn.update_status(axn, &put_in(&1, accessible_observed_generation_key, generation))
    end
  end

  defp accessible_observed_generation_key(observed_generation_key) do
    [last_key | rest] = Enum.reverse(observed_generation_key)

    rest
    |> Enum.map(fn
      key when is_binary(key) -> Access.key(key, %{})
      other -> other
    end)
    |> Enum.reverse([last_key])
  end
end
