lib/nomad_crd/diff_engines/template_diff.ex

defmodule NomadCrd.DiffEngines.TemplateDiff do
  def diff(deployed, template_module) do
    template = template_module.template()

    extract_update_patch(deployed, template)
  end

  def patch(source, patch) do
    patch
    |> Enum.reduce(source, &apply_patch/2)
  end

  def patch_list([], []) do
    []
  end

  def patch_list(source, [{:ins, elemets} | patch_t]) do
    elemets ++ patch_list(source, patch_t)
  end

  def patch_list(source, [{:del, elemets} | patch_t]) do
    source = Enum.drop(source, Enum.count(elemets))
    patch_list(source, patch_t)
  end

  def patch_list([source_h | source_t], [{:no_change} | patch_t]) do
    [source_h | patch_list(source_t, patch_t)]
  end

  def patch_list([source_h | source_t], [patch_h | patch_t]) do
    [patch(source_h, patch_h) | patch_list(source_t, patch_t)]
  end

  def apply_patch({key, value}, acc) when is_atom(value) or is_binary(value) do
    Map.put(acc, key, value)
  end

  def apply_patch({key, value}, acc) when is_list(value) do
    Map.update(acc, key, value, fn source -> patch_list(source, value) end)
  end

  def apply_patch({key, value}, acc) when is_map(value) do
    Map.update(acc, key, value, fn source -> Enum.reduce(value, source, &apply_patch/2) end)
  end

  def extract_update_patch(deployed, template) when is_list(deployed) and is_list(template) do
    list_diff =
      deployed
      |> List.myers_difference(template, fn
        %{} = p1, %{} = p2 -> extract_update_patch(p1, p2)
        _, {:var, _} -> {:no_change}
        _, _ -> nil
      end)
      |> Enum.reduce([], &handle_myer_diff/2)
      |> Enum.reverse()

    all_unchanged? =
      list_diff
      |> Enum.all?(fn
        {:no_change} -> true
        element -> element === %{}
      end)

    if all_unchanged? do
      {:no_change}
    else
      list_diff
    end
  end

  def extract_update_patch(deployed, template) do
    MapDiff.diff(deployed, template)
    |> handle_diff()
  end

  def handle_myer_diff({:ins, elements}, acc) do
    [{:ins, elements} | acc]
  end

  def handle_myer_diff({:del, elements}, acc) do
    [{:del, elements} | acc]
  end

  def handle_myer_diff({:eq, elements}, acc) do
    elements
    |> Enum.reduce(acc, fn _, acc -> [{:no_change} | acc] end)
  end

  def handle_myer_diff({:diff, element}, acc) do
    [element | acc]
  end

  def handle_diff(%{value: diff}) when is_map(diff) do
    Enum.filter(diff, fn
      # Empty Lists will be nulled by nomad
      {_, %{added: [], changed: :primitive_change, removed: nil}} -> false
      # We don't care about equal values
      {_, %{changed: :equal}} -> false
      # Ignore Template values
      {_, %{added: {:var, _}}} -> false
      # Ignore nil Values, as they are set by the Job Struct
      {_, %{added: nil}} -> false
      # Those are the interessing values:
      {_, %{changed: :primitive_change}} -> true
      {_, %{changed: :map_change}} -> true
    end)
    |> Enum.reduce([], fn {key, %{removed: removed, added: added}}, acc ->
      update_patch = extract_update_patch(removed, added)

      case update_patch do
        # nil -> acc
        {:no_change} -> acc
        _ -> [{key, update_patch} | acc]
      end
    end)
    |> Map.new()
  end

  def handle_diff(%{changed: :equal}) do
    {:no_change}
  end

  def handle_diff(%{added: added, changed: :primitive_change}) do
    added
  end
end