lib/glific/flows/message_vars_parser.ex

defmodule Glific.Flows.MessageVarParser do
  @moduledoc """
  substitute the contact fields and result sets in the messages
  """
  require Logger

  alias Glific.{
    Partners,
    Repo
  }

  @doc """
  parse the message with variables
  """
  @spec parse(String.t(), map()) :: String.t() | nil
  def parse(nil, _binding), do: ""

  def parse(input, binding) when binding in [nil, %{}], do: input

  def parse(input, binding) when is_map(binding) == false, do: input

  def parse(input, binding) do
    parser_types = ["@global", "@calendar"]

    binding =
      Enum.reduce(parser_types, binding, fn key, acc ->
        if String.contains?(input, key), do: load_vars(acc, key), else: acc
      end)
      |> stringify_keys()

    input
    |> String.replace(
      ~r/@[\w\-]+[\.][\w\-]+[\.][\w\-]+[\.][\w\-]+[\.][\w\-]*/,
      &bound(&1, binding)
    )
    |> String.replace(~r/@[\w\-]+[\.][\w\-]+[\.][\w\-]+[\.][\w\-]*/, &bound(&1, binding))
    |> String.replace(~r/@[\w\-]+[\.][\w\-]+[\.][\w\-]*/, &bound(&1, binding))
    |> String.replace(~r/@[\w\-]+[\.][\w\-]*/, &bound(&1, binding))
    |> parse_results(binding["results"])
  end

  @spec bound(String.t(), map()) :: String.t()
  defp bound(nil, _binding), do: ""

  defp bound(str, nil), do: str

  # We need to figure out a way to replace these kind of variables
  defp bound("@contact.language", binding) do
    language = get_in(binding, ["contact", "fields", "language"])
    language["label"]
  end

  defp bound("@contact.groups", binding),
    do: bound("@contact.in_groups", binding)

  # since this is a list we need to convert that into a string.
  defp bound("@contact.in_groups", binding) do
    "#{inspect(get_in(binding, ["contact", "in_groups"]))}"
  end

  defp bound(<<_::binary-size(1), var::binary>>, binding) do
    var = String.replace_trailing(var, ".", "")

    substitution =
      get_in(binding, String.split(var, "."))
      |> bound()

    if substitution == nil, do: "@#{var}", else: substitution
  end

  # this is for the other fields like @contact.fields.name which is a map of (value)
  defp bound(substitution) when is_map(substitution) do
    # this is a hack to detect if it a calendar object, and if so, we get the
    # string value. Might need a better solution. This is specifically for inserted_at
    # for now, but generalized so it can handle all datetime objects
    if Map.has_key?(substitution, :calendar),
      do: DateTime.to_string(substitution),
      else: bound(substitution["value"])
  end

  defp bound(substitution), do: substitution

  # """
  # Convert map atom keys to strings
  # """

  @spec stringify_keys(map()) :: map() | nil
  defp stringify_keys(nil), do: nil
  defp stringify_keys(""), do: nil

  defp stringify_keys(atom) when is_atom(atom), do: Atom.to_string(atom)
  defp stringify_keys(map) when is_struct(map), do: Map.from_struct(map)
  defp stringify_keys(int) when is_integer(int), do: Integer.to_string(int)
  defp stringify_keys(float) when is_float(float), do: Float.to_string(float)

  defp stringify_keys(map) when is_map(map) do
    map
    |> Enum.map(fn {k, v} -> {stringify_keys(k), stringify_keys(v)} end)
    |> Enum.into(%{})
  end

  # Walk the list and stringify the keys of
  # of any map members
  defp stringify_keys(list) when is_list(list), do: Enum.map(list, &stringify_keys(&1))

  defp stringify_keys(value), do: value

  @doc """
  Interpolates the values from results into the message body.
  Might need to integrate it with the substitution above.
  It will just treat @results.variable to @results.variable.input
  """
  @spec parse_results(String.t(), map()) :: String.t()
  def parse_results(body, results) when is_map(results) do
    body
    |> do_parse_results("@results.", results)
    |> do_parse_results("@results.parent.", results["parent"])
    |> do_parse_results("@results.child.", results["child"])
  end

  def parse_results(body, _), do: body

  @spec do_parse_one(String.t(), String.t(), map(), String.t()) :: String.t()
  defp do_parse_one(body, replace_prefix, results, key) do
    value = results[key]
    key = String.downcase(key)

    if is_map(value) && Map.has_key?(value, "input") && !is_map(value["input"]) do
      replace = to_string(value["input"])
      String.replace(body, replace_prefix <> key, replace)
    else
      body
    end
  end

  @spec do_parse_results(String.t(), String.t(), map()) :: String.t()
  defp do_parse_results(body, replace_prefix, results) when is_map(results) do
    if String.contains?(body, replace_prefix),
      do:
        results
        |> Map.keys()
        # Sort the keys so we process the larger keys first. this ensures that
        # we handle a key like 'greeting_details' before 'greeting'
        # Issue #1862
        |> Enum.sort(&(byte_size(&1) >= byte_size(&2)))
        |> Enum.reduce(
          body,
          &do_parse_one(&2, replace_prefix, results, &1)
        ),
      else: body
  end

  defp do_parse_results(body, _replace_prefix, _results), do: body

  @doc """
  Replace all the keys and values of a given map
  """
  @spec parse_map(map(), map()) :: map()
  def parse_map(map, bindings) when is_map(map) do
    map
    |> Enum.map(fn {k, v} -> {parse_map(k, bindings), parse_map(v, bindings)} end)
    |> Enum.into(%{})
  end

  def parse_map(value, bindings) when is_list(value),
    do: Enum.map(value, &parse_map(&1, bindings))

  def parse_map(value, bindings) when is_binary(value),
    do: parse(value, bindings) |> parse_results(bindings["results"])

  def parse_map(value, _results), do: value

  defp load_vars(binding, "@global") do
    global_vars =
      Repo.get_organization_id()
      |> Partners.get_global_field_map()

    Map.put(binding, "global", global_vars)
  end

  defp load_vars(binding, "@calendar") do
    default_format = "{D}/{0M}/{YYYY}"
    today = Timex.today()

    calendar_vars = %{
      current_date: today |> Timex.format!(default_format) |> to_string(),
      yesterday: Timex.shift(today, days: -1) |> Timex.format!(default_format) |> to_string(),
      tomorrow: Timex.shift(today, days: 1) |> Timex.format!(default_format) |> to_string(),
      current_day: today |> Timex.weekday() |> Timex.day_name() |> String.downcase(),
      current_month: Timex.now().month |> Timex.month_name() |> String.downcase(),
      current_year: Timex.now().year
    }

    Map.put(binding, "calendar", calendar_vars)
  end

  defp load_vars(binding, _), do: binding
end