lib/glific/tags.ex

defmodule Glific.Tags do
  @moduledoc """
  The Tags Context, which encapsulates and manages tags and the related join tables.
  """
  use Publicist

  alias Glific.{
    Communications,
    Repo,
    Taggers
  }

  alias Glific.Tags.{
    ContactTag,
    MessageTag,
    Tag,
    TemplateTag
  }

  import Ecto.Query

  @doc """
  Returns the list of tags.

  ## Examples

      iex> list_tags()
      [%Tag{}, ...]

  """
  @spec list_tags(map()) :: [Tag.t()]
  def list_tags(args),
    do: Repo.list_filter(args, Tag, &Repo.opts_with_label/2, &Repo.filter_with/2)

  @doc """
  Return the count of tags, using the same filter as list_tags
  """
  @spec count_tags(map()) :: integer
  def count_tags(args),
    do: Repo.count_filter(args, Tag, &Repo.filter_with/2)

  @doc """
  Gets a single tag.

  Raises `Ecto.NoResultsError` if the Tag does not exist.

  ## Examples

      iex> get_tag!(123)
      %Tag{}

      iex> get_tag!(456)
      ** (Ecto.NoResultsError)

  """
  @spec get_tag!(integer) :: Tag.t()
  def get_tag!(id), do: Repo.get!(Tag, id)

  @doc """
  Creates a tag.

  ## Examples

      iex> create_tag(%{field: value})
      {:ok, %Tag{}}

      iex> create_tag(%{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  @spec create_tag(map()) :: {:ok, Tag.t()} | {:error, Ecto.Changeset.t()}
  def create_tag(%{organization_id: organization_id} = attrs) do
    Taggers.reset_tag_maps(organization_id)

    %Tag{}
    |> Tag.changeset(check_shortcode(attrs))
    |> Repo.insert()
  end

  # Adding this so that frontend does not fix it
  # immediately, will remove this very soon
  @spec check_shortcode(map()) :: map()
  defp check_shortcode(%{shortcode: _shortcode} = attrs),
    do: attrs

  defp check_shortcode(%{label: nil} = attrs),
    do: attrs

  defp check_shortcode(%{label: label} = attrs),
    do: Map.update(attrs, :shortcode, Glific.string_clean(label), & &1)

  @doc """
  Updates a tag.

  ## Examples

      iex> update_tag(tag, %{field: new_value})
      {:ok, %Tag{}}

      iex> update_tag(tag, %{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  @spec update_tag(Tag.t(), map()) :: {:ok, Tag.t()} | {:error, Ecto.Changeset.t()}
  def update_tag(%Tag{} = tag, attrs) do
    Taggers.reset_tag_maps(tag.organization_id)

    tag
    |> Tag.changeset(attrs)
    |> Repo.update()
  end

  @doc """
  Deletes a tag.

  ## Examples

      iex> delete_tag(tag)
      {:ok, %Tag{}}

      iex> delete_tag(tag)
      {:error, %Ecto.Changeset{}}

  """
  @spec delete_tag(Tag.t()) :: {:ok, Tag.t()} | {:error, Ecto.Changeset.t()}
  def delete_tag(%Tag{} = tag) do
    tag
    |> Tag.changeset(%{})
    |> Repo.delete()
  end

  @doc """
  Returns an `%Ecto.Changeset{}` for tracking tag changes.

  ## Examples

      iex> change_tag(tag)
      %Ecto.Changeset{data: %Tag{}}

  """
  @spec change_tag(Tag.t(), map()) :: Ecto.Changeset.t()
  def change_tag(%Tag{} = tag, attrs \\ %{}) do
    Tag.changeset(tag, attrs)
  end

  @doc """
  Converts all tag kewords into the map where keyword is the key and tag id is the value
  """
  @spec keyword_map(map()) :: map()
  def keyword_map(%{organization_id: organization_id}) do
    Tag
    |> where([t], not is_nil(t.keywords))
    |> where([t], fragment("array_length(?, 1)", t.keywords) > 0)
    |> where([t], t.organization_id == ^organization_id)
    |> select([:id, :keywords])
    |> Repo.all()
    |> Enum.reduce(%{}, &keyword_map(&1, &2))
  end

  @spec keyword_map(map(), map) :: map()
  defp keyword_map(%{id: tag_id, keywords: keywords}, acc) do
    keywords
    |> Enum.reduce(%{}, &Map.put(&2, &1, tag_id))
    |> Map.merge(acc)
  end

  @doc """
  Filter all the status tag and returns as a map
  """
  @spec status_map(map()) :: %{String.t() => integer}
  def status_map(%{organization_id: organization_id}),
    do:
      Repo.label_id_map(
        Tag,
        ["language", "newcontact"],
        organization_id,
        :shortcode
      )

  @doc """
  Given a tag id or a list of tag ids, retrieve all the ancestors for the list_tags
  """
  @spec include_all_ancestors(non_neg_integer | [non_neg_integer]) :: [non_neg_integer]
  def include_all_ancestors(tag_id) when is_integer(tag_id),
    do: include_all_ancestors([tag_id])

  def include_all_ancestors(tag_ids) do
    Tag
    |> where([t], t.id in ^tag_ids)
    |> select([t], t.ancestors)
    |> Repo.all()
    |> List.flatten()
    |> Enum.concat(tag_ids)
    |> Enum.uniq()
  end

  @doc """
  Given a shortcode of tag, retrieve all the children for the tag
  """
  @spec get_all_children(String.t(), non_neg_integer) :: [Tag.t()]
  def get_all_children(shortcode, organization_id) do
    {:ok, flow_tag} =
      Glific.Repo.fetch_by(Glific.Tags.Tag, %{
        shortcode: shortcode,
        organization_id: organization_id
      })

    Glific.Tags.Tag
    |> where([t], ^flow_tag.id in t.ancestors)
    |> Glific.Repo.all()
  end

  @doc """
  Gets a single message.

  Raises `Ecto.NoResultsError` if the Message does not exist.

  ## Examples

      iex> get_message_tag!(123)
      %Message{}

      iex> get_message_tag!(456)
      ** (Ecto.NoResultsError)

  """
  @spec get_message_tag!(integer) :: MessageTag.t()
  def get_message_tag!(id) do
    Repo.get!(MessageTag, id)
  end

  @doc """
  Creates a message tag

  ## Examples

  iex> create_message_tag(%{field: value})
  {:ok, %Message{}}

  iex> create_message_tag(%{field: bad_value})
  {:error, %Ecto.Changeset{}}

  """
  @spec create_message_tag(map()) :: {:ok, MessageTag.t()} | {:error, Ecto.Changeset.t()}
  def create_message_tag(%{organization_id: organization_id} = attrs) do
    attrs = Map.merge(%{publish: true}, attrs)

    {status, response} =
      %MessageTag{}
      |> MessageTag.changeset(attrs)
      |> Repo.insert(
        on_conflict: :replace_all,
        conflict_target: [:message_id, :tag_id]
      )

    with true <- attrs.publish,
         :ok <- status do
      Communications.publish_data(response, :created_message_tag, organization_id)
    end

    {status, response}
  end

  @doc """
  Updates a message tag.

  ## Examples

      iex> update_message_tag(message_tag, %{field: new_value})
      {:ok, %MessageTag{}}

      iex> update_message_tag(message_tag, %{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  @spec update_message_tag(MessageTag.t(), map()) ::
          {:ok, MessageTag.t()} | {:error, Ecto.Changeset.t()}
  def update_message_tag(%MessageTag{} = message_tag, attrs) do
    message_tag
    |> MessageTag.changeset(attrs)
    |> Repo.update()
  end

  @doc """
  In Join tables we rarely use the table id. We always know the object ids
  and hence more convenient to delete an entry via its object ids.
  We will generalize this function and move it to Repo.ex when we get a better
  handle on how to do so :)
  """
  @spec delete_message_tag_by_ids(integer, [], non_neg_integer) :: {integer(), nil | [term()]}
  def delete_message_tag_by_ids(message_id, tag_ids, organization_id) when is_list(tag_ids) do
    query =
      MessageTag
      |> where([m], m.message_id == ^message_id and m.tag_id in ^tag_ids)

    Repo.all(query)
    |> publish_delete_message_tag(organization_id)

    Repo.delete_all(query)
  end

  @doc """
  Returns an `%Ecto.Changeset{}` for tracking message changes.

  ## Examples

      iex> change_message_tag(message_tag)
      %Ecto.Changeset{data: %MessageTag{}}

  """
  @spec change_message_tag(MessageTag.t(), map()) :: Ecto.Changeset.t()
  def change_message_tag(%MessageTag{} = message_tag, attrs \\ %{}) do
    MessageTag.changeset(message_tag, attrs)
  end

  @doc """
  Gets a single contact.

  Raises `Ecto.NoResultsError` if the Contact does not exist.

  ## Examples

      iex> get_contact_tag!(123)
      %Contact{}

      iex> get_contact_tag!(456)
      ** (Ecto.NoResultsError)

  """
  @spec get_contact_tag!(integer) :: ContactTag.t()
  def get_contact_tag!(id) do
    Repo.get!(ContactTag, id)
  end

  @doc """
  Creates a contact.

  ## Examples

      iex> create_contact_tag(%{field: value})
      {:ok, %Contact{}}

      iex> create_contact_tag(%{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  @spec create_contact_tag(map()) :: {:ok, ContactTag.t()} | {:error, Ecto.Changeset.t()}
  def create_contact_tag(%{organization_id: organization_id} = attrs) do
    {status, response} =
      %ContactTag{}
      |> ContactTag.changeset(attrs)
      |> Repo.insert(on_conflict: :replace_all, conflict_target: [:contact_id, :tag_id])

    if status == :ok,
      do: Communications.publish_data(response, :created_contact_tag, organization_id)

    {status, response}
  end

  @doc """
  Updates a contact tag.

  ## Examples

      iex> update_contact_tag(contact_tag, %{field: new_value})
      {:ok, %ContactTag{}}

      iex> update_contact_tag(contact_tag, %{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  @spec update_contact_tag(ContactTag.t(), map()) ::
          {:ok, ContactTag.t()} | {:error, Ecto.Changeset.t()}
  def update_contact_tag(%ContactTag{} = contact_tag, attrs) do
    contact_tag
    |> ContactTag.changeset(attrs)
    |> Repo.update()
  end

  @doc """
  Returns an `%Ecto.Changeset{}` for tracking contact changes.

  ## Examples

      iex> change_contact_tag(contact_tag)
      %Ecto.Changeset{data: %ContactTag{}}

  """
  @spec change_contact_tag(ContactTag.t(), map()) :: Ecto.Changeset.t()
  def change_contact_tag(%ContactTag{} = contact_tag, attrs \\ %{}) do
    ContactTag.changeset(contact_tag, attrs)
  end

  @doc """
  Remove a specific tag from contact messages
  """
  @spec remove_tag_from_all_message(integer(), String.t(), non_neg_integer, boolean()) :: list()
  def remove_tag_from_all_message(contact_id, tag_shortcode, organization_id, publish \\ true)

  def remove_tag_from_all_message(contact_id, tag_shortcode, organization_id, publish)
      when is_binary(tag_shortcode) do
    remove_tag_from_all_message(contact_id, [tag_shortcode], organization_id, publish)
  end

  @spec remove_tag_from_all_message(integer(), [String.t()], non_neg_integer, boolean()) :: list()
  def remove_tag_from_all_message(contact_id, tag_shortcode_list, organization_id, publish) do
    query =
      from mt in MessageTag,
        join: m in assoc(mt, :message),
        join: t in assoc(mt, :tag),
        where: m.contact_id == ^contact_id,
        where: m.organization_id == ^organization_id,
        where: t.shortcode in ^tag_shortcode_list

    query
    |> Repo.all()
    |> publish_delete_message_tag(organization_id, publish)

    {_, deleted_rows} =
      select(query, [mt], [mt.message_id])
      |> Repo.delete_all()

    List.flatten(deleted_rows)
  end

  @spec publish_delete_message_tag(list, non_neg_integer, boolean()) :: :ok
  defp publish_delete_message_tag(list, organization_id, publish \\ true)
  defp publish_delete_message_tag(_message_tags, _organization_id, false), do: :ok
  defp publish_delete_message_tag([], _organization_id, _publish), do: :ok

  defp publish_delete_message_tag(message_tags, organization_id, true) do
    _list =
      message_tags
      |> Enum.reduce([], fn message_tag, _acc ->
        Communications.publish_data(message_tag, :deleted_message_tag, organization_id)
      end)

    :ok
  end

  @doc """
  Deletes a list of contact tags, each tag attached to the same contact
  """
  @spec delete_contact_tag_by_ids(integer, []) :: {integer(), nil | [term()]}
  def delete_contact_tag_by_ids(contact_id, tag_ids) when is_list(tag_ids) do
    ContactTag
    |> where([m], m.contact_id == ^contact_id and m.tag_id in ^tag_ids)
    |> Repo.delete_all()
  end

  @doc """
  Creates a template tag.

  ## Examples

      iex> create_template_tag(%{field: value})
      {:ok, %Contact{}}

      iex> create_template_tag(%{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  @spec create_template_tag(map()) :: {:ok, TemplateTag.t()} | {:error, Ecto.Changeset.t()}
  def create_template_tag(attrs \\ %{}) do
    %TemplateTag{}
    |> TemplateTag.changeset(attrs)
    |> Repo.insert(
      on_conflict: :replace_all,
      conflict_target: [:template_id, :tag_id]
    )
  end

  @doc """
  Deletes a list of template tags, each tag attached to the same template
  """
  @spec delete_template_tag_by_ids(integer, []) :: {integer(), nil | [term()]}
  def delete_template_tag_by_ids(template_id, tag_ids) when is_list(tag_ids) do
    TemplateTag
    |> where([m], m.template_id == ^template_id and m.tag_id in ^tag_ids)
    |> Repo.delete_all()
  end
end