defmodule BDS.Maintenance.DiffComputation do
  @moduledoc false

  alias BDS.Persistence

  def build_diff_report(entity_type, entity_id, differences) do
    build_diff_report(entity_type, entity_id, differences, [])
  end

  def build_diff_report(entity_type, entity_id, differences, opts) do
    normalized = Enum.reject(differences, &is_nil/1)

    if normalized == [] do
      nil
    else
      %{
        entity_type: entity_type,
        entity_id: entity_id,
        differences: normalized,
        label: Keyword.get(opts, :label),
        meta_label: Keyword.get(opts, :meta_label)
      }
    end
  end

  def metadata_diff_entity_label(title, slug, fallback_id) do
    blank_to_nil(title) || blank_to_nil(slug) || fallback_id
  end

  def metadata_diff_timestamp_label(nil), do: nil
  def metadata_diff_timestamp_label(timestamp), do: Persistence.timestamp_to_iso8601(timestamp)

  def blank_to_nil(nil), do: nil

  def blank_to_nil(value) when is_binary(value) do
    case String.trim(value) do
      "" -> nil
      trimmed -> trimmed
    end
  end

  def blank_to_nil(value), do: value

  def diff_field(name, db_value, file_value) do
    if equal_diff_values?(db_value, file_value) do
      nil
    else
      %{name: name, db_value: stringify_value(db_value), file_value: stringify_value(file_value)}
    end
  end

  def equal_diff_values?(left, right) when is_list(left) and is_list(right) do
    normalize_list_diff_values(left) == normalize_list_diff_values(right)
  end

  def equal_diff_values?(left, right) when is_map(left) and is_map(right) do
    normalize_map_diff_values(left) == normalize_map_diff_values(right)
  end

  def equal_diff_values?(left, right), do: stringify_value(left) == stringify_value(right)

  def normalize_list_diff_values(values) do
    values
    |> Enum.map(&stringify_value/1)
    |> Enum.sort()
  end

  def stringify_value(nil), do: ""
  # Booleans are atoms, so this clause also renders true/false.
  def stringify_value(value) when is_atom(value), do: Atom.to_string(value)
  def stringify_value(value) when is_integer(value), do: Integer.to_string(value)
  def stringify_value(value) when is_binary(value), do: value

  def stringify_value(value) when is_map(value),
    do: value |> normalize_map_diff_values() |> Jason.encode!()

  def stringify_value(value) when is_list(value),
    do: Enum.map_join(value, ",", &stringify_value/1)

  def stringify_value(value), do: to_string(value)

  def normalize_map_diff_values(values) when is_map(values) do
    values
    |> Enum.map(fn {key, value} -> {to_string(key), normalize_nested_diff_value(value)} end)
    |> Enum.sort_by(&elem(&1, 0))
    |> Map.new()
  end

  def normalize_nested_diff_value(value) when is_map(value), do: normalize_map_diff_values(value)

  def normalize_nested_diff_value(value) when is_list(value),
    do: Enum.map(value, &normalize_nested_diff_value/1)

  def normalize_nested_diff_value(value) when is_atom(value), do: Atom.to_string(value)
  def normalize_nested_diff_value(value), do: value
end
