lib/glific_web/flows/flow_editor_controller.ex

defmodule GlificWeb.Flows.FlowEditorController do
  @moduledoc """
  The Flow Editor Controller
  """

  use GlificWeb, :controller

  plug(:set_appsignal_namespace)

  alias Glific.{
    Contacts,
    Dialogflow,
    Flows,
    Flows.ContactField,
    Flows.Flow,
    Flows.FlowCount,
    Flows.FlowLabel,
    GCS.GcsWorker,
    Partners,
    Repo,
    Settings,
    Sheets,
    Templates.InteractiveTemplate,
    Templates.InteractiveTemplates,
    Users.User
  }

  defp set_appsignal_namespace(conn, _params) do
    # Configures all actions in this controller to report
    Glific.Appsignal.set_namespace("flow_editor_controller")
    conn
  end

  @doc false
  @spec globals(Plug.Conn.t(), map) :: Plug.Conn.t()
  def globals(conn, _params) do
    conn
    |> json(%{results: []})
  end

  @doc false
  @spec groups(Plug.Conn.t(), map) :: Plug.Conn.t()
  def groups(conn, _params) do
    group_list =
      Glific.Groups.list_groups(
        %{filter: %{organization_id: conn.assigns[:organization_id]}},
        true
      )
      |> Enum.reduce([], fn group, acc ->
        [%{uuid: "#{group.id}", name: group.label, type: "group"} | acc]
      end)

    conn
    |> json(%{results: group_list})
  end

  @doc false
  @spec groups_post(Plug.Conn.t(), map) :: Plug.Conn.t()
  def groups_post(conn, _params) do
    conn
    |> json(%{
      uuid: 0,
      query: nil,
      status: "not-ready",
      count: 0,
      name: "ALERT: PLEASE CREATE NEW GROUP FROM THE ORGANIZATION SETTINGS"
    })
  end

  @doc false
  @spec fields(Plug.Conn.t(), map) :: Plug.Conn.t()
  def fields(conn, _params) do
    fields =
      ContactField.list_contacts_fields(%{
        filter: %{organization_id: conn.assigns[:organization_id]}
      })
      |> Enum.reduce([], fn cf, acc ->
        [%{key: cf.shortcode, name: cf.name, value_type: cf.value_type, label: cf.name} | acc]
      end)

    json(conn, %{results: fields})
  end

  @doc """
  Add Contact fields into the database. The response should be a map with 3 keys
  % { Key: Field name, name: Field display name value_type: type of the value}

  We are not supporting this for now. We will add that in future
  """

  @spec fields_post(Plug.Conn.t(), map) :: Plug.Conn.t()
  def fields_post(conn, params) do
    # need to store this into DB, the value_type will default to text in this case
    # the shortcode is the name, lower cased, and camelized
    ContactField.create_contact_field(%{
      name: params["label"],
      shortcode: String.downcase(params["label"]) |> String.replace(" ", "_"),
      organization_id: conn.assigns[:organization_id]
    })
    |> case do
      {:ok, contact_field} ->
        conn
        |> json(%{
          key: contact_field.shortcode,
          name: contact_field.name,
          label: contact_field.name,
          value_type: contact_field.value_type
        })

      {:error, _} ->
        conn
        |> put_status(400)
        |> json(%{
          error: %{status: 400, message: "Cannot create new field with label #{params["label"]}"}
        })
    end
  end

  @doc """
    Get all the tags so that user can apply them on incoming message.
    We are not supporting this for now. To enable It should return a list of map having
    uuid and name as keys
    [%{uuid: tag.uuid, name: tag.label}]
  """
  @spec labels(Plug.Conn.t(), nil | maybe_improper_list | map) :: Plug.Conn.t()
  def labels(conn, _params) do
    flow_list = FlowLabel.get_all_flowlabel(conn.assigns[:organization_id])

    json(conn, %{results: flow_list})
  end

  @doc """
  Store a label (new tag) in the system. The return response should be a map of 3 keys.
  [%{uuid: tag.uuid, name: params["name"], count}]

  We are not supporting them for now. We will come back to this in near future
  """
  @spec labels_post(Plug.Conn.t(), nil | maybe_improper_list | map) :: Plug.Conn.t()
  def labels_post(conn, params) do
    {:ok, flow_label} =
      FlowLabel.create_flow_label(%{
        name: params["name"],
        organization_id: conn.assigns[:organization_id]
      })

    json(conn, %{uuid: flow_label.uuid, name: flow_label.name, count: 0})
  end

  @doc """
  A list of all the communication channels. For Glific it's just WhatsApp.
  We are not supporting them for now. We will come back to this in near future
  """
  @spec channels(Plug.Conn.t(), nil | maybe_improper_list | map) :: Plug.Conn.t()
  def channels(conn, _params) do
    channels = %{
      results: [
        %{
          uuid: generate_uuid(),
          name: "WhatsApp",
          address: "",
          schemes: ["whatsapp"],
          roles: ["send", "receive"]
        }
      ]
    }

    json(conn, channels)
  end

  @doc """
  A list of all the NLP classifiers. For Glific it's just Dialogflow
  We are not supporting them for now. We will come back to this in near future
  """
  @spec classifiers(Plug.Conn.t(), nil | maybe_improper_list | map) :: Plug.Conn.t()
  def classifiers(conn, _params) do
    organization_id = conn.assigns[:organization_id]

    classifiers = %{
      results: [
        %{
          uuid: "dialogflow_uuid",
          name: "Dialogflow",
          type: "dialogflow",
          intents: Dialogflow.Intent.get_intent_name_list(organization_id)
        }
      ]
    }

    json(conn, classifiers)
  end

  @doc """
  We are not sure how to use this but this endpoint is required for flow editor.
  Will come back to this in future.
  """
  @spec ticketers(Plug.Conn.t(), nil | maybe_improper_list | map) :: Plug.Conn.t()
  def ticketers(conn, _params) do
    ticketers = %{results: []}
    json(conn, ticketers)
  end

  @doc """
    We are not using this for now but this is required for flow editor config.
  """
  @spec resthooks(Plug.Conn.t(), nil | maybe_improper_list | map) :: Plug.Conn.t()
  def resthooks(conn, _params) do
    resthooks = %{results: []}
    json(conn, resthooks)
  end

  @doc """
  A list of all the interactive templates in format that is understood by flow editor
  """
  @spec interactive_templates(Plug.Conn.t(), nil | maybe_improper_list | map) :: Plug.Conn.t()
  def interactive_templates(conn, _params) do
    results =
      InteractiveTemplates.list_interactives(%{
        filter: %{organization_id: conn.assigns[:organization_id]}
      })
      |> Enum.reduce([], fn interactive, acc ->
        [
          %{
            id: interactive.id,
            name: interactive.label,
            type: interactive.type,
            interactive_content: interactive.interactive_content,
            created_on: interactive.inserted_at,
            modified_on: interactive.updated_at
          }
          | acc
        ]
      end)

    json(conn, %{results: results})
  end

  @doc """
  Fetching single interactive template and returning in format that is understood by flow editor
  or
  Return error Interactive message not found
  """
  @spec interactive_template(Plug.Conn.t(), nil | maybe_improper_list | map) :: Plug.Conn.t()
  def interactive_template(conn, params) do
    [id] = params["vars"]
    {:ok, id} = Glific.parse_maybe_integer(id)

    case Repo.fetch_by(InteractiveTemplate, %{id: id}) do
      {:ok, interactive_template} ->
        %{
          id: interactive_template.id,
          name: interactive_template.label,
          type: interactive_template.type,
          interactive_content: interactive_template.interactive_content,
          created_on: interactive_template.inserted_at,
          modified_on: interactive_template.updated_at,
          translations: get_interactive_translations(interactive_template.translations)
        }
        |> then(&json(conn, &1))

      {:error, _} ->
        json(conn, %{error: "Interactive message not found"})
    end
  end

  @spec get_interactive_translations(map) :: map()
  defp get_interactive_translations(interactive_translations) do
    language_map = Settings.get_language_id_local_map()

    interactive_translations
    |> Enum.map(fn {language_id, value} -> %{language_map[language_id] => value} end)
    |> Enum.reduce(%{}, fn translation, acc -> Map.merge(acc, translation) end)
  end

  @doc false
  @spec templates(Plug.Conn.t(), nil | maybe_improper_list | map) :: Plug.Conn.t()
  def templates(conn, _params) do
    results =
      Glific.Templates.list_session_templates(%{
        filter: %{organization_id: conn.assigns[:organization_id], status: "APPROVED"}
      })
      |> Enum.reduce([], fn template, acc ->
        template = Glific.Repo.preload(template, :language)
        language = template.language

        [
          %{
            uuid: template.uuid,
            name: template.label,
            created_on: template.inserted_at,
            modified_on: template.updated_at,
            translations:
              Enum.concat(
                [
                  %{
                    language: language.locale,
                    content: template.body,
                    variable_count: template.number_parameters,
                    status: "approved",
                    channel: %{uuid: "", name: "WhatsApp"}
                  }
                ],
                get_template_translations(template.translations)
              )
          }
          | acc
        ]
      end)

    json(conn, %{results: results})
  end

  @spec get_template_translations(nil | map) :: list()
  defp get_template_translations(nil), do: []

  defp get_template_translations(template_translations) do
    language_map = Settings.get_language_id_local_map()

    template_translations
    |> Enum.reduce([], fn {language_id, translation}, acc ->
      [
        %{
          content: translation["body"],
          variable_count: translation["number_parameters"],
          status: "approved",
          language: language_map[language_id],
          channel: %{uuid: "", name: "WhatsApp"}
        }
        | acc
      ]
    end)
  end

  @doc false
  @spec languages(Plug.Conn.t(), nil | maybe_improper_list | map) :: Plug.Conn.t()
  def languages(conn, _params) do
    results =
      Glific.Partners.organization(conn.assigns[:organization_id]).languages
      |> Enum.reduce([], fn language, acc ->
        [%{iso: language.locale, name: language.label} | acc]
      end)

    json(conn, %{results: results})
  end

  @doc false
  @spec environment(Plug.Conn.t(), nil | maybe_improper_list | map) :: Plug.Conn.t()
  def environment(conn, _params) do
    environment = %{}
    json(conn, environment)
  end

  @doc false
  @spec recipients(Plug.Conn.t(), nil | maybe_improper_list | map) :: Plug.Conn.t()
  def recipients(conn, _params) do
    # we should return only staff contact ids here
    # ideally should only be able to send them an HSM template, so we need
    # this to be fixed in the frontend
    recipients =
      Contacts.list_user_contacts()
      |> Enum.reduce([], fn c, acc ->
        [%{id: "#{c.id}", name: c.name, type: "contact", extra: c.id} | acc]
      end)

    json(conn, %{results: recipients})
  end

  @doc """
  instead of reading a file we can call it directly from Assets.
  We will come back on that when we have more clarity of the use cases
  """
  @spec completion(Plug.Conn.t(), nil | maybe_improper_list | map) :: Plug.Conn.t()
  def completion(conn, _params) do
    completion =
      File.read!(Path.join(:code.priv_dir(:glific), "data/flows/completion.json"))
      |> Jason.decode!()

    functions =
      File.read!(Path.join(:code.priv_dir(:glific), "data/flows/functions.json"))
      |> Jason.decode!()

    results = %{
      context: completion,
      functions: functions
    }

    json(conn, results)
  end

  @doc """
  This is used to checking if the connection between frontend and backend is established or not.
  """
  @spec activity(Plug.Conn.t(), nil | maybe_improper_list | map) :: Plug.Conn.t()
  def activity(conn, params) do
    {nodes, segments, recent_messages} =
      FlowCount.get_flow_count_list(params["flow"])
      |> Enum.reduce(
        {%{}, %{}, %{}},
        fn fc, acc ->
          {nodes, segments, recent_messages} = acc

          case fc.type do
            "node" ->
              {Map.put(nodes, fc.uuid, fc.count), segments, recent_messages}

            "exit" ->
              key = "#{fc.uuid}:#{fc.destination_uuid}"

              {
                nodes,
                Map.put(segments, key, fc.count),
                Map.put(recent_messages, key, get_recent_message(fc))
              }

            _ ->
              acc
          end
        end
      )

    activity = %{nodes: nodes, segments: segments, recentMessages: recent_messages}
    json(conn, activity)
  end

  @spec get_recent_message(FlowCount.t()) :: list()
  defp get_recent_message(flow_count) do
    # flow editor shows only last 3 messages. We are just tacking 5 for the safe side.
    flow_count.recent_messages
    |> Enum.map(fn msg -> %{text: msg["message"], sent: msg["date"]} end)
    |> Enum.take(5)
  end

  @doc """
  Let's get all the flows or a latest flow revision
  """
  @spec flows(Plug.Conn.t(), nil | maybe_improper_list | map) :: Plug.Conn.t()
  def flows(conn, %{"vars" => vars}) do
    results =
      case vars do
        [] ->
          ## We need to fix this before merging this branch
          Flows.list_flows(%{
            filter: %{organization_id: conn.assigns[:organization_id], status: "published"}
          })
          |> Enum.reduce([], fn flow, acc ->
            [
              %{
                uuid: flow.uuid,
                name: flow.name,
                type: flow.flow_type,
                archived: false,
                labels: [],
                expires: 10_080,
                parent_refs: []
              }
              | acc
            ]
          end)

        [flow_uuid] ->
          with {:ok, flow} <-
                 Repo.fetch_by(Flow, %{
                   uuid: flow_uuid,
                   organization_id: conn.assigns[:organization_id]
                 }),
               do: Flow.get_latest_definition(flow.id)
      end

    json(conn, %{results: results})
  end

  @doc """
    Get all or a specific revision for a flow
  """
  @spec revisions(Plug.Conn.t(), nil | maybe_improper_list | map) :: Plug.Conn.t()
  def revisions(conn, %{"vars" => vars}) do
    case vars do
      [flow_uuid] -> json(conn, Flows.get_flow_revision_list(flow_uuid))
      [flow_uuid, revision_id] -> json(conn, Flows.get_flow_revision(flow_uuid, revision_id))
    end
  end

  @doc """
    Save a revision for a flow and get the revision id
  """
  @spec save_revisions(Plug.Conn.t(), nil | maybe_improper_list | map) :: Plug.Conn.t()
  def save_revisions(conn, params) do
    revision = Flows.create_flow_revision(params)
    json(conn, %{revision: revision.id})
  end

  @doc """
    Validate media to send as attachment
  """
  @spec validate_media(Plug.Conn.t(), nil | maybe_improper_list | map) :: Plug.Conn.t()
  def validate_media(conn, params) do
    json(conn, Glific.Messages.validate_media(params["url"], params["type"]))
  end

  @doc false
  @spec attachments_enabled(Plug.Conn.t(), nil | maybe_improper_list | map) :: Plug.Conn.t()
  def attachments_enabled(conn, _) do
    organization_id = conn.assigns[:organization_id]
    json(conn, %{is_enabled: Partners.attachments_enabled?(organization_id)})
  end

  @doc false
  @spec flow_attachment(Plug.Conn.t(), nil | maybe_improper_list | map) :: Plug.Conn.t()
  def flow_attachment(conn, %{"media" => media, "extension" => extension} = _params) do
    organization_id = conn.assigns[:organization_id]

    remote_name =
      conn.assigns[:current_user]
      |> remote_name(extension)

    res =
      GcsWorker.upload_media(media.path, remote_name, organization_id)
      |> case do
        {:ok, media} -> %{url: media.url, type: media.type, error: nil}
        {:error, error} -> %{url: nil, error: error}
      end

    json(conn, res)
  end

  @doc false
  @spec sheets(Plug.Conn.t(), nil | maybe_improper_list | map) :: Plug.Conn.t()
  def sheets(conn, _params) do
    results =
      Sheets.list_sheets(%{
        filter: %{organization_id: conn.assigns[:organization_id], is_active: true}
      })
      |> Enum.reduce([], fn sheet, acc ->
        [
          %{
            id: sheet.id,
            name: sheet.label,
            url: sheet.url
          }
          | acc
        ]
      end)

    json(conn, %{results: results})
  end

  @doc false
  @spec recents(Plug.Conn.t(), nil | maybe_improper_list | map) :: Plug.Conn.t()
  def recents(conn, params) do
    [exit_uuid, destination_uuid, flow_uuid] = params["vars"]

    {:ok, flow_count} =
      Repo.fetch_by(FlowCount, %{
        uuid: exit_uuid,
        destination_uuid: destination_uuid,
        flow_uuid: flow_uuid,
        organization_id: conn.assigns[:organization_id]
      })

    results =
      Enum.reduce(flow_count.recent_messages, [], fn recent_message, acc ->
        [
          %{
            contact: recent_message["contact"],
            operand: recent_message["message"],
            time: recent_message["date"]
          }
          | acc
        ]
      end)

    json(conn, results)
  end

  @doc false
  @spec generate_uuid() :: String.t()
  defp generate_uuid do
    Ecto.UUID.generate()
  end

  @spec remote_name(User.t() | nil, String.t(), Ecto.UUID.t()) :: String.t()
  defp remote_name(user, extension, uuid \\ Ecto.UUID.generate()) do
    {year, week} = Timex.iso_week(Timex.now())
    "outbound/#{year}-#{week}/#{user.name}/#{uuid}.#{extension}"
  end
end