defmodule Bonny.Resource do
  @moduledoc """
  Helper functions for dealing with kubernetes resources.
  """

  @type t :: map()

  @doc """
  Get a reference to the given resource

  ### Example

      iex> resource = %{
      ...>   "apiVersion" => "example.com/v1",
      ...>   "kind" => "Orange",
      ...>   "metadata" => %{
      ...>     "name" => "annoying",
      ...>     "namespace" => "default",
      ...>     "uid" => "e19b6f40-3293-11ed-a261-0242ac120002"
      ...>   }
      ...> }
      ...> Bonny.Resource.resource_reference(resource)
      %{
        "apiVersion" => "example.com/v1",
        "kind" => "Orange",
        "name" => "annoying",
        "namespace" => "default",
        "uid" => "e19b6f40-3293-11ed-a261-0242ac120002"
      }
  """
  def resource_reference(nil), do: nil

  def resource_reference(resource) do
    ref = %{
      "apiVersion" => K8s.Resource.FieldAccessors.api_version(resource),
      "kind" => K8s.Resource.FieldAccessors.kind(resource),
      "name" => K8s.Resource.FieldAccessors.name(resource),
      "uid" => get_in(resource, ~w(metadata uid))
    }

    namespace = get_in(resource, ~w(metadata namespace))
    if is_nil(namespace), do: ref, else: Map.put(ref, "namespace", namespace)
  end

  @doc """
  Add an owner reference to the given resource.

  ### Example

      iex> owner = %{
      ...>   "apiVersion" => "example.com/v1",
      ...>   "kind" => "Orange",
      ...>   "metadata" => %{
      ...>     "name" => "annoying",
      ...>     "namespace" => "default",
      ...>     "uid" => "e19b6f40-3293-11ed-a261-0242ac120002"
      ...>   }
      ...> }
      ...> resource = %{
      ...>   "apiVersion" => "v1",
      ...>   "kind" => "Pod",
      ...>   "metadata" => %{"name" => "nginx", "namespace" => "default"}
      ...>   # spec
      ...> }
      ...> Bonny.Resource.add_owner_reference(resource, owner)
      %{
        "apiVersion" => "v1",
        "kind" => "Pod",
        "metadata" => %{
          "name" => "nginx",
          "namespace" => "default",
          "ownerReferences" => [%{
            "apiVersion" => "example.com/v1",
            "blockOwnerDeletion" => false,
            "controller" => true,
            "kind" => "Orange",
            "name" => "annoying",
            "uid" => "e19b6f40-3293-11ed-a261-0242ac120002"
          }]
        }
      }
  """
  @spec add_owner_reference(t(), map(), keyword(boolean)) :: t()
  def add_owner_reference(resource, owner, opts \\ [])

  def add_owner_reference(%{"metadata" => _} = resource, owner, opts) do
    owner_ref = owner_reference(owner, opts)

    put_in(
      resource,
      [
        "metadata",
        Access.key("ownerReferences", []),
        K8s.Resource.NamedList.access(owner_ref["name"])
      ],
      owner_ref
    )
  end

  def add_owner_reference(%{metadata: _} = resource, owner, opts) do
    owner_ref = owner_reference(owner, opts)

    put_in(
      resource,
      [
        :metadata,
        Access.key(:ownerReferences, []),
        K8s.Resource.NamedList.access(owner_ref["name"])
      ],
      owner_ref
    )
  end

  defp owner_reference(resource, opts) do
    resource
    |> resource_reference()
    |> Map.delete("namespace")
    |> Map.put("blockOwnerDeletion", Keyword.get(opts, :block_owner_deletion, false))
    |> Map.put("controller", true)
  end

  @doc """
  Set a label on the given resource.

  ### Example

      iex> resource = %{
      ...>   "apiVersion" => "v1",
      ...>   "kind" => "Pod",
      ...>   "metadata" => %{"name" => "nginx", "namespace" => "default"}
      ...>   # spec
      ...> }
      ...> Bonny.Resource.set_label(resource, "app.kubernetes.io/managed-by", "my-operator")
      %{
        "apiVersion" => "v1",
        "kind" => "Pod",
        "metadata" => %{
          "name" => "nginx",
          "namespace" => "default",
          "labels" => %{
            "app.kubernetes.io/managed-by" => "my-operator"
          }
        }
      }
  """
  @spec set_label(t(), binary(), binary()) :: t()
  def set_label(resource, label, value) do
    put_in(
      resource,
      ["metadata", Access.key("labels", %{}), label],
      value
    )
  end

  @doc """
  Sets .status.observedGeneration to .metadata.generation
  """
  @spec set_observed_generation(t()) :: t()
  def set_observed_generation(resource) do
    generation = get_in(resource, ~w(metadata generation))
    put_in(resource, [Access.key("status", %{}), "observedGeneration"], generation)
  end

  @doc """
  Removes .metadata.managedFields from the resource.
  """
  @spec drop_managed_fields(t()) :: t()
  def drop_managed_fields(resource),
    do: Map.update!(resource, "metadata", &Map.delete(&1, "managedFields"))

  @doc """
  Removes .metadata.resourceVersion from the resource.
  """
  @spec drop_rv(t()) :: t()
  def drop_rv(resource),
    do: Map.update!(resource, "metadata", &Map.delete(&1, "resourceVersion"))

  @doc """
  Applies the given resource to the cluster.
  """
  @spec apply(t(), K8s.Conn.t(), Keyword.t()) :: K8s.Client.Runner.Base.result_t()
  def apply(resource, conn, opts) do
    opts =
      Keyword.merge([force: true], opts)
      |> Keyword.put_new_lazy(:field_manager, fn -> Bonny.Config.name() end)

    K8s.Client.apply(resource, opts)
    |> K8s.Client.put_conn(conn)
    |> K8s.Client.run()
  end

  @doc """
  Applies the given resource to the cluster.
  """
  @spec apply_async(list(t()), K8s.Conn.t(), Keyword.t()) ::
          list({t(), K8s.Client.Runner.Base.result_t()})
  def apply_async(resources, conn, opts \\ []) do
    opts =
      Keyword.merge([force: true], opts)
      |> Keyword.put_new_lazy(:field_manager, fn -> Bonny.Config.name() end)

    ops = Enum.map(resources, &K8s.Client.apply(&1, opts))
    results = K8s.Client.async(conn, ops)
    Enum.zip(resources, results)
  end

  @doc """
  Applies the status subresource of the given resource to the cluster.
  If the given resource doesn't contain a status object, nothing is done and
  :noop is returned.
  """
  @spec apply_status(t(), K8s.Conn.t(), Keyword.t()) :: K8s.Client.Runner.Base.result_t() | :noop
  def apply_status(resource, conn, opts \\ [])

  def apply_status(resource, conn, opts)
      when is_map_key(resource, "status") or is_map_key(resource, :status) do
    opts =
      Keyword.merge([force: true], opts)
      |> Keyword.put_new_lazy(:field_manager, fn -> Bonny.Config.name() end)

    resource =
      resource
      |> drop_managed_fields()
      |> drop_rv()

    op =
      K8s.Client.apply(
        resource["apiVersion"],
        {resource["kind"], "Status"},
        [
          namespace: get_in(resource, ~w(metadata namespace)),
          name: get_in(resource, ~w(metadata name))
        ],
        resource,
        opts
      )

    K8s.Client.run(conn, op)
  end

  def apply_status(_, _, _), do: :noop

  @doc """
  Returns a tuple in the form

  * {apiVersion, kind, namespace/name} for namespaced resources
  * {apiVersion, kind, name} for cluster scoped resources
  """
  @spec gvkn(t()) :: {namespace_name :: binary(), api_version :: binary(), kind :: binary()}
  def gvkn(resource) do
    ns_name =
      String.trim_leading(
        "#{resource["metadata"]["namespace"]}/#{resource["metadata"]["name"]}",
        "/"
      )

    {ns_name, resource["apiVersion"], "Kind=#{resource["kind"]}"}
  end
end
