# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Hashtag do
  use Ecto.Schema

  import Ecto.Changeset
  import Ecto.Query

  alias Ecto.Multi
  alias Pleroma.Hashtag
  alias Pleroma.User.HashtagFollow
  alias Pleroma.Object
  alias Pleroma.Repo

  schema "hashtags" do
    field(:name, :string)

    many_to_many(:objects, Object, join_through: "hashtags_objects", on_replace: :delete)

    timestamps()
  end

  def normalize_name(name) do
    name
    |> String.downcase()
    |> String.trim()
  end

  def get_by_id(id) do
    Repo.get(Hashtag, id)
  end

  def get_by_name(name) do
    Repo.get_by(Hashtag, name: normalize_name(name))
  end

  def get_or_create_by_name(name) do
    changeset = changeset(%Hashtag{}, %{name: name})

    Repo.insert(
      changeset,
      on_conflict: [set: [name: get_field(changeset, :name)]],
      conflict_target: :name,
      returning: true
    )
  end

  def get_or_create_by_names(names) when is_list(names) do
    names = Enum.map(names, &normalize_name/1)
    timestamp = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)

    structs =
      Enum.map(names, fn name ->
        %Hashtag{}
        |> changeset(%{name: name})
        |> Map.get(:changes)
        |> Map.merge(%{inserted_at: timestamp, updated_at: timestamp})
      end)

    try do
      with {:ok, %{query_op: hashtags}} <-
             Multi.new()
             |> Multi.insert_all(:insert_all_op, Hashtag, structs,
               on_conflict: :nothing,
               conflict_target: :name
             )
             |> Multi.run(:query_op, fn _repo, _changes ->
               {:ok, Repo.all(from(ht in Hashtag, where: ht.name in ^names))}
             end)
             |> Repo.transaction() do
        {:ok, hashtags}
      else
        {:error, _name, value, _changes_so_far} -> {:error, value}
      end
    rescue
      e -> {:error, e}
    end
  end

  def changeset(%Hashtag{} = struct, params) do
    struct
    |> cast(params, [:name])
    |> update_change(:name, &normalize_name/1)
    |> validate_required([:name])
    |> unique_constraint(:name)
  end

  def unlink(%Object{id: object_id}) do
    with {_, hashtag_ids} <-
           from(hto in "hashtags_objects",
             where: hto.object_id == ^object_id,
             select: hto.hashtag_id
           )
           |> Repo.delete_all(),
         {:ok, unreferenced_count} <- delete_unreferenced(hashtag_ids) do
      {:ok, length(hashtag_ids), unreferenced_count}
    end
  end

  @delete_unreferenced_query """
  DELETE FROM hashtags WHERE id IN
    (SELECT hashtags.id FROM hashtags
      LEFT OUTER JOIN hashtags_objects
        ON hashtags_objects.hashtag_id = hashtags.id
      WHERE hashtags_objects.hashtag_id IS NULL AND hashtags.id = ANY($1));
  """

  def delete_unreferenced(ids) do
    with {:ok, %{num_rows: deleted_count}} <- Repo.query(@delete_unreferenced_query, [ids]) do
      {:ok, deleted_count}
    end
  end

  def get_followers(%Hashtag{id: hashtag_id}) do
    from(hf in HashtagFollow)
    |> where([hf], hf.hashtag_id == ^hashtag_id)
    |> join(:inner, [hf], u in assoc(hf, :user))
    |> select([hf, u], u.id)
    |> Repo.all()
  end

  def get_recipients_for_activity(%Pleroma.Activity{object: %{hashtags: tags}})
      when is_list(tags) do
    tags
    |> Enum.map(&get_followers/1)
    |> List.flatten()
    |> Enum.uniq()
  end

  def get_recipients_for_activity(_activity), do: []
end
