defmodule Mobilizon.Posts do
  @moduledoc """
  The Posts context.
  """
  alias Mobilizon.Actors.Actor
  alias Mobilizon.Events.Tag
  alias Mobilizon.Posts.Post
  alias Mobilizon.Service.Export.Cachable
  alias Mobilizon.Storage.{Page, Repo}

  import Ecto.Query
  require Logger

  @post_preloads [:author, :attributed_to, :picture, :media, :tags]

  import EctoEnum

  defenum(PostVisibility, :post_visibility, [
    :public,
    :unlisted,
    :restricted,
    :private
  ])

  @spec list_public_local_posts(integer | nil, integer | nil, atom | nil, atom | nil) ::
          Page.t(Post.t())
  def list_public_local_posts(page \\ nil, limit \\ nil, sort \\ nil, direction \\ nil) do
    Post
    |> filter_public()
    |> filter_local()
    |> order_posts(sort, direction)
    |> preload_post_associations()
    |> Page.build_page(page, limit)
  end

  @spec list_posts_for_stream :: Enum.t()
  def list_posts_for_stream do
    Post
    |> filter_public()
    |> Repo.stream()
  end

  @doc """
  Returns the list of recent posts for a group
  """
  @spec get_posts_for_group(Actor.t(), integer | nil, integer | nil) :: Page.t(Post.t())
  def get_posts_for_group(%Actor{id: group_id}, page \\ nil, limit \\ nil) do
    group_id
    |> do_get_posts_for_group()
    |> Page.build_page(page, limit)
  end

  @spec get_public_posts_for_group(Actor.t(), integer | nil, integer | nil) :: Page.t(Post.t())
  def get_public_posts_for_group(%Actor{id: group_id}, page \\ nil, limit \\ nil) do
    group_id
    |> do_get_posts_for_group()
    |> filter_public()
    |> Page.build_page(page, limit)
  end

  @doc """
  Get a post by it's ID
  """
  @spec get_post(integer | String.t()) :: Post.t() | nil
  def get_post(nil), do: nil
  def get_post(id), do: Repo.get(Post, id)

  @spec get_post_with_preloads(integer | String.t()) :: Post.t() | nil
  def get_post_with_preloads(id) do
    Post
    |> Repo.get(id)
    |> Repo.preload(@post_preloads)
  end

  @spec get_post_by_slug_with_preloads(String.t()) :: Post.t() | nil
  def get_post_by_slug_with_preloads(slug) do
    Post
    |> Repo.get_by(slug: slug)
    |> Repo.preload(@post_preloads)
  end

  @doc """
  Get a post by it's URL
  """
  @spec get_post_by_url(String.t()) :: Post.t() | nil
  def get_post_by_url(url), do: Repo.get_by(Post, url: url)

  @spec get_post_by_url_with_preloads(String.t()) :: Post.t() | nil
  def get_post_by_url_with_preloads(url) do
    Post
    |> Repo.get_by(url: url)
    |> Repo.preload(@post_preloads)
  end

  @doc """
  Creates a post.
  """
  @spec create_post(map) :: {:ok, Post.t()} | {:error, Ecto.Changeset.t()}
  def create_post(attrs \\ %{}) do
    with {:ok, %Post{} = post} <-
           %Post{}
           |> Post.changeset(attrs)
           |> Repo.insert() do
      {:ok, Repo.preload(post, @post_preloads)}
    end
  end

  @doc """
  Updates a post.
  """
  @spec update_post(Post.t(), map) :: {:ok, Post.t()} | {:error, Ecto.Changeset.t()}
  def update_post(%Post{} = post, attrs) do
    Cachable.clear_all_caches(post)

    with {:ok, %Post{} = post} <-
           post
           |> Repo.preload(@post_preloads)
           |> Post.changeset(attrs)
           |> Repo.update() do
      {:ok, Repo.preload(post, @post_preloads)}
    end
  end

  @doc """
  Deletes a post
  """
  @spec delete_post(Post.t()) :: {:ok, Post.t()} | {:error, Ecto.Changeset.t()}
  def delete_post(%Post{} = post) do
    Cachable.clear_all_caches(post)
    Repo.delete(post)
  end

  @doc """
  Returns the list of tags for the post.
  """
  @spec list_tags_for_post(integer | String.t()) :: [Tag.t()]
  def list_tags_for_post(post_id) do
    {:ok, uuid} = Ecto.UUID.dump(post_id)

    uuid
    |> tags_for_post_query()
    |> Repo.all()
  end

  @spec tags_for_post_query(String.t()) :: Ecto.Query.t()
  defp tags_for_post_query(post_id) do
    from(
      t in Tag,
      join: p in "posts_tags",
      on: t.id == p.tag_id,
      where: p.post_id == ^post_id
    )
  end

  @spec filter_public(Ecto.Queryable.t()) :: Ecto.Query.t()
  defp filter_public(query) do
    where(query, [p], p.visibility == ^:public and not p.draft)
  end

  @spec filter_local(Ecto.Queryable.t()) :: Ecto.Query.t()
  defp filter_local(query) do
    where(query, [q], q.local == true)
  end

  defp do_get_posts_for_group(group_id) do
    Post
    |> where(attributed_to_id: ^group_id)
    |> order_by(desc: :inserted_at)
    |> preload_post_associations()
  end

  @spec preload_post_associations(Ecto.Queryable.t(), list()) :: Ecto.Query.t()
  defp preload_post_associations(query, associations \\ @post_preloads) do
    preload(query, ^associations)
  end

  @spec order_posts(Ecto.Queryable.t(), atom | nil, atom | nil) :: Ecto.Queryable.t()
  def order_posts(query, nil, _direction), do: query
  def order_posts(query, _sort, nil), do: query

  def order_posts(query, sort, direction) do
    order_by_param = Keyword.new([{direction, sort}])
    order_by(query, ^order_by_param)
  end
end