defmodule Memex.Contexts do
  @moduledoc """
  The Contexts context.
  """

  use Memex, :context
  alias Memex.Contexts.Context

  @doc """
  Returns the list of contexts.

  ## Examples

      iex> list_contexts(%User{id: 123})
      [%Context{}, ...]

      iex> list_contexts("my context", %User{id: 123})
      [%Context{slug: "my context"}, ...]

  """
  @spec list_contexts(User.t()) :: [Context.t()]
  @spec list_contexts(search :: String.t() | nil, User.t()) :: [Context.t()]
  def list_contexts(search \\ nil, user)

  def list_contexts(search, %{id: user_id}) when user_id |> is_binary() and search in [nil, ""] do
    Repo.all(from c in Context, order_by: c.slug)
  end

  def list_contexts(search, %{id: user_id})
      when user_id |> is_binary() and search |> is_binary() do
    trimmed_search = String.trim(search)

    Repo.all(
      from c in Context,
        where:
          fragment(
            "search @@ websearch_to_tsquery('english', ?)",
            ^trimmed_search
          ),
        order_by: {
          :desc,
          fragment(
            "ts_rank_cd(search, websearch_to_tsquery('english', ?), 4)",
            ^trimmed_search
          )
        }
    )
  end

  @doc """
  Returns the list of public contexts for viewing.

  ## Examples

      iex> list_public_contexts()
      [%Context{}, ...]

      iex> list_public_contexts("my context")
      [%Context{slug: "my context"}, ...]

  """
  @spec list_public_contexts() :: [Context.t()]
  @spec list_public_contexts(search :: String.t() | nil) :: [Context.t()]
  def list_public_contexts(search \\ nil)

  def list_public_contexts(search) when search in [nil, ""] do
    Repo.all(from c in Context, where: c.visibility == :public, order_by: c.slug)
  end

  def list_public_contexts(search) when search |> is_binary() do
    trimmed_search = String.trim(search)

    Repo.all(
      from c in Context,
        where: c.visibility == :public,
        where:
          fragment(
            "search @@ websearch_to_tsquery('english', ?)",
            ^trimmed_search
          ),
        order_by: {
          :desc,
          fragment(
            "ts_rank_cd(search, websearch_to_tsquery('english', ?), 4)",
            ^trimmed_search
          )
        }
    )
  end

  @doc """
  Returns the list of contexts that link to a particular slug.

  ## Examples

      iex> backlink(%User{id: 123})
      [%Context{}, ...]

      iex> backlink("[other-context]", %User{id: 123})
      [%Context{content: "[other-context]"}, ...]

  """
  @spec backlink(String.t(), User.t()) :: [Context.t()]
  def backlink(link, %{id: user_id}) when user_id |> is_binary() do
    link = link |> String.replace("[", "\\[") |> String.replace("]", "\\]")
    link_regex = "(^|[^\[])#{link}($|[^\]])"

    Repo.all(
      from c in Context,
        where: fragment("? ~ ?", c.content, ^link_regex),
        order_by: c.slug
    )
  end

  def backlink(link, _invalid_user) do
    link = link |> String.replace("[", "\\[") |> String.replace("]", "\\]")
    link_regex = "(^|[^\[])#{link}($|[^\]])"

    Repo.all(
      from c in Context,
        where: fragment("? ~ ?", c.content, ^link_regex),
        where: c.visibility == :public,
        order_by: c.slug
    )
  end

  @doc """
  Gets a single context.

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

  ## Examples

      iex> get_context!(123, %User{id: 123})
      %Context{}

      iex> get_context!(456, %User{id: 123})
      ** (Ecto.NoResultsError)

  """
  @spec get_context!(Context.id(), User.t()) :: Context.t()
  def get_context!(id, %{id: user_id}) when user_id |> is_binary() do
    Repo.one!(from c in Context, where: c.id == ^id)
  end

  def get_context!(id, _invalid_user) do
    Repo.one!(
      from c in Context,
        where: c.id == ^id,
        where: c.visibility in [:public, :unlisted]
    )
  end

  @doc """
  Gets a single context by a slug.

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

  ## Examples

      iex> get_context_by_slug("my-context", %User{id: 123})
      %Context{}

      iex> get_context_by_slug("my-context", %User{id: 123})
      ** (Ecto.NoResultsError)

  """
  @spec get_context_by_slug(Context.slug(), User.t()) :: Context.t() | nil
  def get_context_by_slug(slug, %{id: user_id}) when user_id |> is_binary() do
    Repo.one(from c in Context, where: c.slug == ^slug)
  end

  def get_context_by_slug(slug, _invalid_user) do
    Repo.one(
      from c in Context,
        where: c.slug == ^slug,
        where: c.visibility in [:public, :unlisted]
    )
  end

  @doc """
  Creates a context.

  ## Examples

      iex> create_context(%{field: value}, %User{id: 123})
      {:ok, %Context{}}

      iex> create_context(%{field: bad_value}, %User{id: 123})
      {:error, %Ecto.Changeset{}}

  """
  @spec create_context(User.t()) :: {:ok, Context.t()} | {:error, Context.changeset()}
  @spec create_context(attrs :: map(), User.t()) ::
          {:ok, Context.t()} | {:error, Context.changeset()}
  def create_context(attrs \\ %{}, user) do
    Context.create_changeset(attrs, user) |> Repo.insert()
  end

  @doc """
  Updates a context.

  ## Examples

      iex> update_context(context, %{field: new_value}, %User{id: 123})
      {:ok, %Context{}}

      iex> update_context(context, %{field: bad_value}, %User{id: 123})
      {:error, %Ecto.Changeset{}}

  """
  @spec update_context(Context.t(), attrs :: map(), User.t()) ::
          {:ok, Context.t()} | {:error, Context.changeset()}
  def update_context(%Context{} = context, attrs, user) do
    context
    |> Context.update_changeset(attrs, user)
    |> Repo.update()
  end

  @doc """
  Deletes a context.

  ## Examples

      iex> delete_context(%Context{}, %User{id: 123})
      {:ok, %Context{}}

      iex> delete_context(%Context{}, nil)
      {:error, %Ecto.Changeset{}}

  """
  @spec delete_context(Context.t(), User.t()) ::
          {:ok, Context.t()} | {:error, Context.changeset()}
  def delete_context(%Context{} = context, %{id: user_id}) when user_id |> is_binary() do
    context |> Repo.delete()
  end

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

  ## Examples

      iex> change_context(context)
      %Ecto.Changeset{data: %Context{}}

  """
  @spec change_context(Context.t(), User.t()) :: Context.changeset()
  @spec change_context(Context.t(), attrs :: map(), User.t()) :: Context.changeset()
  def change_context(%Context{} = context, attrs \\ %{}, user) do
    context |> Context.update_changeset(attrs, user)
  end
end