lib/glific/clients/tap.ex

defmodule Glific.Clients.Tap do
  @moduledoc """
  Tweak GCS Bucket name based on group that the contact is in (if any)
  """

  import Ecto.Query, warn: false

  alias Glific.{
    Contacts.Contact,
    Flows.ContactField,
    Groups.ContactGroup,
    Groups.Group,
    Partners,
    Partners.OrganizationData,
    Repo,
    Sheets.ApiClient,
    Templates.SessionTemplate
  }

  @props %{
    sheet_links: %{
      activity:
        "https://docs.google.com/spreadsheets/d/e/2PACX-1vR-GBWadR2F3QKZ43jaUwS9WYy0QQ5n_AMW4FN5AziwrEuNcfFr5__5zsO1nMNX04M1BmvChBaXTU9r/pub?gid=2079471637&single=true&output=csv",
      quiz:
        "https://docs.google.com/spreadsheets/d/e/2PACX-1vR-GBWadR2F3QKZ43jaUwS9WYy0QQ5n_AMW4FN5AziwrEuNcfFr5__5zsO1nMNX04M1BmvChBaXTU9r/pub?gid=720505613&single=true&output=csv"
    }
  }

  @doc """
  In the case of TAP we retrive the first group the contact is in and store
  and set the remote name to be a sub-directory under that group (if one exists)
  """
  @spec gcs_file_name(map()) :: String.t()
  def gcs_file_name(media) do
    group_name =
      Contact
      |> where([c], c.id == ^media["contact_id"])
      |> join(:inner, [c], cg in ContactGroup, on: c.id == cg.contact_id)
      |> join(:inner, [_c, cg], g in Group, on: cg.group_id == g.id)
      |> select([_c, _cg, g], g.label)
      |> order_by([_c, _cg, g], g.label)
      |> first()
      |> Repo.one()

    if is_nil(group_name),
      do: media["remote_name"],
      else: group_name <> "/" <> media["remote_name"]
  end

  @doc """
  get template form EEx without variables
  """
  @spec template(String.t(), String.t()) :: binary
  def template(shortcode, params_staring \\ "") do
    [template | _tail] =
      SessionTemplate
      |> where([st], st.shortcode == ^shortcode)
      |> Repo.all()

    %{
      uuid: template.uuid,
      name: "Template",
      expression: nil,
      variables: parse_template_vars(template, params_staring)
    }
    |> Jason.encode!()
  end

  defp parse_template_vars(template, params_staring) do
    params = String.split(params_staring || "", "|", trim: true)

    if length(params) == template.number_parameters do
      params
    else
      params_with_missing =
        params ++ Enum.map(1..template.number_parameters, fn _i -> "{{ missing var  }}" end)

      Enum.take(params_with_missing, template.number_parameters)
    end
  end

  @doc """
  Create a webhook with different signatures, so we can easily implement
  additional functionality as needed
  """
  @spec webhook(String.t(), map()) :: map()
  def webhook("load_activities", fields) do
    Glific.parse_maybe_integer!(fields["organization_id"])
    |> load_activities()

    fields
  end

  def webhook("load_quizes", fields) do
    Glific.parse_maybe_integer!(fields["organization_id"])
    |> load_quizzes()

    fields
  end

  def webhook("get_activity_info", fields) do
    org_id = Glific.parse_maybe_integer!(fields["organization_id"])
    course = get_in(fields, ["contact", "fields", "course", "value"]) || fields["type"]
    date = get_in(fields, ["contact", "fields", "test_date", "value"]) || to_string(Timex.today())

    get_activity_info(org_id, date, course, fields["language_label"])
    |> maybe_add_profile_activity(fields["contact"]["id"], org_id)
  end

  def webhook("get_quiz_info", fields) do
    Glific.parse_maybe_integer!(fields["organization_id"])
    |> get_quiz_info(fields["activity_id"])
  end

  def webhook("get_quiz_question", fields) do
    Glific.parse_maybe_integer!(fields["organization_id"])
    |> get_quiz_question(fields["activity_id"], fields["question_id"], fields["language_label"])
  end

  def webhook("validate_question_answer", fields) do
    user_input = Glific.string_clean(fields["user_input"])
    correct_answer = Glific.string_clean(fields["correct_answer"])

    in_valid_answer_range =
      fields["valid_answers"]
      |> String.split("|", trim: true)
      |> Enum.map(&Glific.string_clean(&1))
      |> Enum.member?(user_input)

    cond do
      user_input == correct_answer ->
        %{status: "correct_response"}

      correct_answer == "allanswers" && in_valid_answer_range ->
        %{status: "correct_response"}

      in_valid_answer_range ->
        %{status: "incorrect_response"}

      true ->
        %{status: "out_of_range"}
    end
  end

  def webhook("mark_activity_as_complete", fields) do
    Glific.parse_maybe_integer!(fields["organization_id"])
    |> mark_activity_as_complete(
      fields["activity_id"],
      fields["contact"]["id"]
    )
  end

  def webhook("update_all_profile_fields", fields) do
    org_id = Glific.parse_maybe_integer!(fields["organization_id"])
    contact_id = Glific.parse_maybe_integer!(fields["contact"]["id"])
    update_all_profile_fields(org_id, contact_id, fields["key"], fields["value"])
    fields
  end

  def webhook("save_school_name", fields) do
    org_id = Glific.parse_maybe_integer!(fields["organization_id"])
    contact_id = Glific.parse_maybe_integer!(fields["contact"]["id"])
    school_name = fields["school_name"] || ""
    formatted_school_name = String.split(school_name, [" ", ","])

    school_short_form =
      if length(formatted_school_name) > 1 do
        Enum.reduce(formatted_school_name, "", fn val, acc ->
          acc <> String.first(String.capitalize(val))
        end)
      else
        hd(formatted_school_name)
      end

    key = "school_" <> Glific.string_clean(school_name)

    info = %{name: school_name, generated_by: contact_id, school_short_form: school_short_form}

    {:ok, data} = Partners.maybe_insert_organization_data(key, info, org_id)
    new_key = Glific.string_clean(school_short_form) <> to_string(data.id)

    data
    |> Ecto.Changeset.cast(%{key: new_key}, [:key])
    |> Repo.update!()

    waba_link = "https://api.whatsapp.com/send?phone=918454812392&text=tapschool:" <> new_key

    %{
      is_valid: true,
      waba_link: waba_link,
      school_key: new_key,
      formatted_key: "tapschool:" <> new_key
    }
  end

  def webhook("get_school_name", fields) do
    org_id = Glific.parse_maybe_integer!(fields["organization_id"])

    key =
      fields["school_name"]
      |> String.replace("tapschool:", "")
      |> Glific.string_clean()

    Repo.fetch_by(OrganizationData, %{
      organization_id: org_id,
      key: key
    })
    |> case do
      {:ok, data} ->
        Map.merge(
          %{
            "is_valid" => true,
            "message" => "School found",
            "key" => key
          },
          data.json
        )

      _ ->
        %{
          "is_valid" => false,
          "key" => key,
          "message" => "School not found."
        }
    end
  end

  def webhook(_, fields), do: fields

  @spec load_activities(non_neg_integer()) :: :ok
  defp load_activities(org_id) do
    ApiClient.get_csv_content(url: @props.sheet_links.activity)
    |> Enum.each(fn {_, row} ->
      row = clean_row_values(row)
      activity_type = Glific.string_clean(row["Activity type"])
      key = "schedule_" <> row["Schedule"] <> "_" <> activity_type

      info = %{activity_type => row}
      Partners.maybe_insert_organization_data(key, info, org_id)
    end)
  end

  @spec load_quizzes(non_neg_integer()) :: :ok
  defp load_quizzes(org_id) do
    ApiClient.get_csv_content(url: @props.sheet_links.quiz)
    |> Enum.each(fn {_, row} ->
      row = clean_row_values(row)
      question_key = Glific.string_clean(row["Question Id"])
      key = "quiz_" <> row["Activity"] <> "_" <> question_key
      row = Map.put(row, "question_key", question_key)
      Partners.maybe_insert_organization_data(key, row, org_id)
    end)
  end

  @spec get_activity_info(non_neg_integer(), String.t(), String.t(), String.t()) :: map()
  defp get_activity_info(org_id, date, type, language_label) do
    type = Glific.string_clean(type)

    key = "schedule_" <> date <> "_" <> type

    Repo.fetch_by(OrganizationData, %{
      organization_id: org_id,
      key: key
    })
    |> case do
      {:ok, data} ->
        data.json[type]
        |> clean_map_keys()
        |> format_hsm_templates(language_label)
        |> Map.merge(%{
          "is_valid" => true,
          "message" => "Activity found"
        })

      _ ->
        %{
          "is_valid" => false,
          "message" => "Worksheet code not found"
        }
    end
  end

  defp get_quiz_question(org_id, activity_id, question_id, language_label) do
    Repo.fetch_by(OrganizationData, %{
      organization_id: org_id,
      key: "quiz_" <> activity_id <> "_" <> question_id
    })
    |> case do
      {:ok, data} ->
        data.json
        |> clean_map_keys()
        |> format_quiz_question(language_label)

      _ ->
        %{
          is_valid: false,
          message: "Question not found"
        }
    end
  end

  defp format_quiz_question(question_data, language_label) do
    questions_answers =
      case language_label do
        "English" ->
          %{
            valid_answers: question_data["validresponsesenglish"],
            correct_response: question_data["answerenglish"],
            question: question_data["questionmessageenglish"],
            attachment_url: question_data["attachmentenglish"]
          }

        "Hindi" ->
          %{
            valid_answers: question_data["validresponseshindi"],
            correct_response: question_data["answerhindi"],
            question: question_data["questionmessagehindi"],
            attachment_url: question_data["attachmenthindi"]
          }

        "Kannada" ->
          %{
            valid_answers: question_data["validresponseshindi"],
            correct_response: question_data["answerhindi"],
            question: question_data["questionmessagehindi"],
            attachment_url: question_data["attachmentenglish"]
          }

        "Marathi" ->
          %{
            valid_answers: question_data["validresponsesmarathi"],
            correct_response: question_data["answermarathi"],
            question: question_data["questionmessagemarathi"],
            attachment_url: question_data["attachmentenglish"]
          }

        _ ->
          %{}
      end

    buttons =
      questions_answers.valid_answers
      |> String.split("|")
      |> Enum.with_index()
      |> Enum.map(fn {answer, index} -> {"button_#{index + 1}", answer} end)
      |> Enum.into(%{})

    Map.merge(question_data, %{
      buttons: buttons,
      button_count: length(Map.keys(buttons)),
      is_valid: true
    })
    |> Map.merge(questions_answers)
  end

  @spec get_quiz_info(non_neg_integer(), String.t()) :: map()
  defp get_quiz_info(org_id, activity_id) do
    quizzes =
      Partners.list_organization_data(%{
        organization_id: org_id,
        filter: %{
          key: "quiz_" <> activity_id
        }
      })

    Enum.reduce(quizzes, %{}, fn row, acc ->
      data = row.json
      Map.put(acc, data["question_key"], clean_map_keys(data))
    end)
  end

  defp format_hsm_templates(activity_info, language_label) do
    templates =
      case language_label do
        "English" ->
          %{
            intro: %{
              shortcode: activity_info["introtemplateuuidenglish"],
              params: activity_info["introtemplatevariablesenglish"]
            },
            intro_no_response: %{
              shortcode: activity_info["intronoresponsenudgetemplateuuidenglish"],
              params: activity_info["intronoresponsenudgetemplatevariablesenglish"]
            },
            submission_first_no_response: %{
              shortcode: activity_info["activitysubmissionfirstnoresponsetemplatemessageenglish"],
              params: activity_info["activitysubmissionfirstnoresponsetemplatevariablesenglish"]
            },
            submission_second_no_response: %{
              shortcode: activity_info["activitysubmissionsecondnoresponsetemplateuuidenglish"],
              params: activity_info["activitysubmissionsecondnoresponsetemplatevariablesenglish"]
            }
          }

        "Hindi" ->
          %{
            intro: %{
              shortcode: activity_info["introtemplateuuidhindi"],
              params: activity_info["introtemplatevariableshindi"]
            },
            intro_no_response: %{
              shortcode: activity_info["intronoresponsenudgetemplateuuidhindi"],
              params: activity_info["intronoresponsenudgetemplatevariableshindi"]
            },
            submission_first_no_response: %{
              shortcode: activity_info["activitysubmissionfirstnoresponsetemplatemessagehindi"],
              params: activity_info["activitysubmissionfirstnoresponsetemplatevariableshindi"]
            },
            submission_second_no_response: %{
              shortcode: activity_info["activitysubmissionsecondnoresponsetemplateuuidhindi"],
              params: activity_info["activitysubmissionsecondnoresponsetemplatevariableshindi"]
            }
          }

        "Marathi" ->
          %{
            intro: %{
              shortcode: activity_info["introtemplateuuidmarathi"],
              params: activity_info["introtemplatevariablesmarathi"]
            },
            intro_no_response: %{
              shortcode: activity_info["intronoresponsenudgetemplateuuidmarathi"],
              params: activity_info["intronoresponsenudgetemplatevariablesmarathi"]
            },
            submission_first_no_response: %{
              shortcode: activity_info["activitysubmissionfirstnoresponsetemplatemessagemarathi"],
              params: activity_info["activitysubmissionfirstnoresponsetemplatevariablesmarathi"]
            },
            submission_second_no_response: %{
              shortcode: activity_info["activitysubmissionsecondnoresponsetemplateuuidmarathi"],
              params: activity_info["activitysubmissionsecondnoresponsetemplatevariablesmarathi"]
            }
          }

        _ ->
          %{}
      end

    Map.merge(activity_info, templates)
  end

  @spec clean_map_keys(map()) :: map()
  defp clean_map_keys(data) do
    data
    |> Enum.map(fn {k, v} -> {Glific.string_clean(k), v} end)
    |> Enum.into(%{})
  end

  @spec mark_activity_as_complete(non_neg_integer(), String.t(), non_neg_integer()) :: map()
  defp mark_activity_as_complete(_org_id, activity_id, contact_id) do
    contact = Repo.get!(Contact, contact_id)
    completed_activities = get_in(contact.fields, ["completed_activities", "value"])

    completed_activities =
      if is_nil(completed_activities),
        do: "#{activity_id}",
        else: "#{completed_activities}, #{activity_id}"

    contact =
      ContactField.do_add_contact_field(
        contact,
        "completed_activities",
        "completed_activities",
        completed_activities
      )

    completed_activities_count =
      completed_activities
      |> String.split(",", trim: true)
      |> Enum.uniq_by(&String.trim(&1))
      |> length()

    ContactField.do_add_contact_field(
      contact,
      "completed_activities_count",
      "completed_activities_count",
      completed_activities_count
    )

    %{
      completed_activities: completed_activities,
      completed_activities_count: completed_activities_count
    }
  end

  @spec clean_row_values(map()) :: map()
  defp clean_row_values(row) do
    row
    |> Enum.map(fn
      {k, v} when is_list(v) -> {k, String.replace(hd(v), ~r/\n\r\n/, "\n")}
      {k, v} -> {k, String.replace(v, ~r/\n\r\n/, "\n")}
    end)
    |> Enum.into(%{})
  end

  @doc """
    Check if a contact has more profiles and add that to message.
  """
  @spec maybe_add_profile_activity(map(), non_neg_integer(), non_neg_integer()) :: map()
  def maybe_add_profile_activity(activity_info, contact_id, org_id) do
    {:ok, contact} = Repo.fetch_by(Contact, %{id: contact_id, organization_id: org_id})

    if is_nil(contact.active_profile_id) do
      activity_info
    else
      profile_activities =
        %{filter: %{contact_id: contact.id}, organization_id: org_id}
        |> Glific.Profiles.list_profiles()
        |> get_profile_activities(org_id)

      activity_msg_eng = Map.get(profile_activities, :english_messages, []) |> Enum.join("\n\n")
      activity_msg_hin = Map.get(profile_activities, :hindi_messages, []) |> Enum.join("\n\n")

      Map.merge(activity_info, %{
        profiles_activity_message_english: activity_msg_eng,
        profiles_activity_message_hindi: activity_msg_hin,
        has_profile_activity_message: activity_msg_eng != ""
      })
    end
  end

  @spec get_profile_activities(list(), non_neg_integer()) :: map()
  defp get_profile_activities(profiles, org_id) do
    profiles
    |> Enum.reduce(%{english_messages: [], hindi_messages: [], already_processed: []}, fn profile,
                                                                                          acc ->
      test_date = profile.fields["test_date"]["value"]
      course = profile.fields["course"]["value"]
      already_processed = Map.get(acc, :already_processed, [])
      english_messages = Map.get(acc, :english_messages, [])
      hindi_messages = Map.get(acc, :hindi_messages, [])

      profile_activity =
        if Enum.member?(already_processed, course) == false do
          get_activity_info(org_id, test_date, course, "English")
        else
          %{"is_valid" => false}
        end

      if profile_activity["is_valid"] do
        english_messages = english_messages ++ [profile_activity["activitymainmessageenglish"]]
        hindi_messages = hindi_messages ++ [profile_activity["activitymainmessagehindi"]]
        already_processed = already_processed ++ [course]

        acc
        |> Map.put(:english_messages, english_messages)
        |> Map.put(:hindi_messages, hindi_messages)
        |> Map.put(:already_processed, already_processed)
      else
        acc
        |> Map.put(:already_processed, already_processed ++ [course])
      end
    end)
  end

  @doc """
    Update the fields for all the profiles.
  """
  @spec update_all_profile_fields(non_neg_integer(), non_neg_integer(), String.t(), String.t()) ::
          :ok
  def update_all_profile_fields(org_id, contact_id, key, value) do
    {:ok, contact} = Repo.fetch_by(Contact, %{id: contact_id, organization_id: org_id})

    %{
      filter: %{contact_id: contact.id},
      organization_id: org_id
    }
    |> Glific.Profiles.list_profiles()
    |> Enum.each(fn profile ->
      new_fields = %{
        key => %{
          "inserted_at" => DateTime.utc_now(),
          "value" => value,
          "type" => "string",
          "label" => key
        }
      }

      fields = Map.merge(profile.fields, new_fields)
      Glific.Profiles.update_profile(profile, %{fields: fields})
    end)

    ContactField.do_add_contact_field(contact, key, key, value)
    :ok
  end

  @doc """
  Fix the contact name issue
  """
  @spec fix_contact_name :: :ok
  def fix_contact_name do
    %{
      filter: %{},
      organization_id: 12
    }
    |> Glific.Profiles.list_profiles()
    |> Enum.each(fn profile ->
      fields = profile.fields

      contact_name = %{
        "contact_name" => %{
          "inserted_at" => "2022-08-03T13:43:21.134329Z",
          "value" => profile.name,
          "type" => "string",
          "label" => "contact name"
        },
        "name" => %{
          "inserted_at" => "2022-08-03T13:43:21.134329Z",
          "value" => profile.name,
          "type" => "string",
          "label" => "Name"
        }
      }

      fields = Map.merge(fields, contact_name)
      Glific.Profiles.update_profile(profile, %{fields: fields})
    end)
  end
end