defmodule Cannery.Invites do
  @moduledoc """
  The Invites context.
  """

  import Ecto.Query, warn: false
  alias Cannery.{Accounts.User, Invites.Invite, Repo}

  @invite_token_length 20

  @doc """
  Returns the list of invites.

  ## Examples

      iex> list_invites(%User{id: 123, role: :admin})
      [%Invite{}, ...]

  """
  @spec list_invites(User.t()) :: [Invite.t()]
  def list_invites(%User{role: :admin}) do
    Repo.all(from i in Invite, order_by: i.name)
  end

  @doc """
  Gets a single invite.

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

  ## Examples

      iex> get_invite!(123, %User{id: 123, role: :admin})
      %Invite{}

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

  """
  @spec get_invite!(Invite.id(), User.t()) :: Invite.t()
  def get_invite!(id, %User{role: :admin}) do
    Repo.get!(Invite, id)
  end

  @doc """
  Returns a valid invite or nil based on the attempted token

  ## Examples

      iex> get_invite_by_token("valid_token")
      %Invite{}

      iex> get_invite_by_token("invalid_token")
      nil
  """
  @spec get_invite_by_token(token :: String.t() | nil) :: Invite.t() | nil
  def get_invite_by_token(nil), do: nil
  def get_invite_by_token(""), do: nil

  def get_invite_by_token(token) do
    Repo.one(
      from(i in Invite,
        where: i.token == ^token and i.disabled_at |> is_nil()
      )
    )
  end

  @doc """
  Uses invite by decrementing uses_left, or marks invite invalid if it's been
  completely used.
  """
  @spec use_invite!(Invite.t()) :: Invite.t()
  def use_invite!(%Invite{uses_left: nil} = invite), do: invite

  def use_invite!(%Invite{uses_left: uses_left} = invite) do
    new_uses_left = uses_left - 1

    attrs =
      if new_uses_left <= 0 do
        now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
        %{"uses_left" => 0, "disabled_at" => now}
      else
        %{"uses_left" => new_uses_left}
      end

    invite |> Invite.update_changeset(attrs) |> Repo.update!()
  end

  @doc """
  Creates a invite.

  ## Examples

      iex> create_invite(%User{id: 123, role: :admin}, %{field: value})
      {:ok, %Invite{}}

      iex> create_invite(%User{id: 123, role: :admin}, %{field: bad_value})
      {:error, %Changeset{}}

  """
  @spec create_invite(User.t(), attrs :: map()) ::
          {:ok, Invite.t()} | {:error, Invite.changeset()}
  def create_invite(%User{role: :admin} = user, attrs) do
    token =
      :crypto.strong_rand_bytes(@invite_token_length)
      |> Base.url_encode64()
      |> binary_part(0, @invite_token_length)

    Invite.create_changeset(user, token, attrs) |> Repo.insert()
  end

  @doc """
  Updates a invite.

  ## Examples

      iex> update_invite(invite, %{field: new_value}, %User{id: 123, role: :admin})
      {:ok, %Invite{}}

      iex> update_invite(invite, %{field: bad_value}, %User{id: 123, role: :admin})
      {:error, %Changeset{}}

  """
  @spec update_invite(Invite.t(), attrs :: map(), User.t()) ::
          {:ok, Invite.t()} | {:error, Invite.changeset()}
  def update_invite(invite, attrs, %User{role: :admin}),
    do: invite |> Invite.update_changeset(attrs) |> Repo.update()

  @doc """
  Deletes a invite.

  ## Examples

      iex> delete_invite(invite, %User{id: 123, role: :admin})
      {:ok, %Invite{}}

      iex> delete_invite(invite, %User{id: 123, role: :admin})
      {:error, %Changeset{}}

  """
  @spec delete_invite(Invite.t(), User.t()) ::
          {:ok, Invite.t()} | {:error, Invite.changeset()}
  def delete_invite(invite, %User{role: :admin}), do: invite |> Repo.delete()

  @doc """
  Deletes a invite.

  ## Examples

      iex> delete_invite(invite, %User{id: 123, role: :admin})
      %Invite{}

  """
  @spec delete_invite!(Invite.t(), User.t()) :: Invite.t()
  def delete_invite!(invite, %User{role: :admin}), do: invite |> Repo.delete!()
end
