defmodule Mobilizon.Applications do
  @moduledoc """
  The Applications context.
  """

  import Ecto.Query, warn: false
  import EctoEnum
  alias Ecto.Multi
  alias Mobilizon.Applications.Application
  alias Mobilizon.Storage.Repo

  defenum(ApplicationDeviceActivationStatus, [
    "success",
    "pending",
    "confirmed",
    "incorrect_device_code",
    "access_denied"
  ])

  defenum(ApplicationTokenStatus, [
    "success",
    "pending",
    "access_denied"
  ])

  @doc """
  Returns the list of applications.

  ## Examples

      iex> list_applications()
      [%Application{}, ...]

  """
  def list_applications do
    Repo.all(Application)
  end

  @doc """
  Gets a single application.

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

  ## Examples

      iex> get_application!(123)
      %Application{}

      iex> get_application!(456)
      ** (Ecto.NoResultsError)

  """
  def get_application!(id), do: Repo.get!(Application, id)

  @doc """
  Gets a single application.

  Returns nil if the Application does not exist.

  ## Examples

      iex> get_application_by_client_id(123)
      %Application{}

      iex> get_application_by_client_id(456)
      nil

  """
  def get_application_by_client_id(client_id), do: Repo.get_by(Application, client_id: client_id)

  @doc """
  Creates a application.

  ## Examples

      iex> create_application(%{field: value})
      {:ok, %Application{}}

      iex> create_application(%{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def create_application(attrs \\ %{}) do
    %Application{}
    |> Application.changeset(attrs)
    |> Repo.insert()
  end

  @doc """
  Updates a application.

  ## Examples

      iex> update_application(application, %{field: new_value})
      {:ok, %Application{}}

      iex> update_application(application, %{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def update_application(%Application{} = application, attrs) do
    application
    |> Application.changeset(attrs)
    |> Repo.update()
  end

  @doc """
  Deletes a application.

  ## Examples

      iex> delete_application(application)
      {:ok, %Application{}}

      iex> delete_application(application)
      {:error, %Ecto.Changeset{}}

  """
  def delete_application(%Application{} = application) do
    Repo.delete(application)
  end

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

  ## Examples

      iex> change_application(application)
      %Ecto.Changeset{data: %Application{}}

  """
  def change_application(%Application{} = application, attrs \\ %{}) do
    Application.changeset(application, attrs)
  end

  alias Mobilizon.Applications.ApplicationToken

  @doc """
  Returns the list of application_tokens.

  ## Examples

      iex> list_application_tokens()
      [%ApplicationToken{}, ...]

  """
  def list_application_tokens do
    ApplicationToken
    |> Repo.all()
    |> Repo.preload(:application)
  end

  @doc """
  Returns the list of application tokens for a given user_id
  """
  @spec list_application_tokens_for_user_id(number() | String.t()) :: list(ApplicationToken.t())
  def list_application_tokens_for_user_id(user_id) do
    ApplicationToken
    |> where(user_id: ^user_id)
    |> where([at], is_nil(at.authorization_code))
    |> preload(:application)
    |> Repo.all()
  end

  @doc """
  Gets a single application_token.

  Raises `Ecto.NoResultsError` if the Application token does not exist.

  ## Examples

      iex> get_application_token!(123)
      %ApplicationToken{}

      iex> get_application_token!(456)
      ** (Ecto.NoResultsError)

  """
  def get_application_token!(id) do
    ApplicationToken
    |> Repo.get!(id)
    |> Repo.preload([:application])
  end

  @doc """
  Gets a single application_token.

  ## Examples

      iex> get_application_token(123)
      %ApplicationToken{}

      iex> get_application_token(456)
      nil

  """
  def get_application_token(application_token_id),
    do: Repo.get(ApplicationToken, application_token_id)

  def get_application_token(app_id, user_id),
    do: Repo.get_by(ApplicationToken, application_id: app_id, user_id: user_id)

  def get_application_token_by_authorization_code(code),
    do: Repo.get_by(ApplicationToken, authorization_code: code)

  @doc """
  Creates a application_token.

  ## Examples

      iex> create_application_token(%{field: value})
      {:ok, %ApplicationToken{}}

      iex> create_application_token(%{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def create_application_token(attrs \\ %{}) do
    %ApplicationToken{}
    |> ApplicationToken.changeset(attrs)
    |> Repo.insert(on_conflict: :replace_all, conflict_target: [:user_id, :application_id])
    |> case do
      {:ok, application_token} ->
        {:ok, Repo.preload(application_token, :application)}

      error ->
        error
    end
  end

  @doc """
  Updates a application_token.

  ## Examples

      iex> update_application_token(application_token, %{field: new_value})
      {:ok, %ApplicationToken{}}

      iex> update_application_token(application_token, %{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def update_application_token(%ApplicationToken{} = application_token, attrs) do
    application_token
    |> ApplicationToken.changeset(attrs)
    |> Repo.update()
  end

  @doc """
  Deletes a application_token.

  ## Examples

      iex> delete_application_token(application_token)
      {:ok, %ApplicationToken{}}

      iex> delete_application_token(application_token)
      {:error, %Ecto.Changeset{}}

  """
  def delete_application_token(%ApplicationToken{} = application_token) do
    Repo.delete(application_token)
  end

  def revoke_application_token(%ApplicationToken{id: app_token_id} = application_token) do
    Multi.new()
    |> Multi.delete_all(
      :delete_guardian_tokens,
      from(gt in "guardian_tokens", where: gt.sub == ^"AppToken:#{app_token_id}")
    )
    |> Multi.delete(:delete_app_token, application_token)
    |> Repo.transaction()
  end

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

  ## Examples

      iex> change_application_token(application_token)
      %Ecto.Changeset{data: %ApplicationToken{}}

  """
  def change_application_token(%ApplicationToken{} = application_token, attrs \\ %{}) do
    ApplicationToken.changeset(application_token, attrs)
  end

  @spec prune_old_application_tokens(pos_integer()) :: {non_neg_integer(), nil}
  def prune_old_application_tokens(lifetime) do
    exp = DateTime.add(DateTime.utc_now(), -lifetime)

    ApplicationToken
    |> where([at], at.status != :success)
    |> where([at], at.inserted_at < ^exp)
    |> Repo.delete_all()
  end

  alias Mobilizon.Applications.ApplicationDeviceActivation

  @doc """
  Returns the list of application_device_activation.

  ## Examples

      iex> list_application_device_activation()
      [%ApplicationDeviceActivation{}, ...]

  """
  def list_application_device_activation do
    Repo.all(ApplicationDeviceActivation)
  end

  @doc """
  Gets a single application_device_activation.

  Raises `Ecto.NoResultsError` if the Application device activation does not exist.

  ## Examples

      iex> get_application_device_activation!(123)
      %ApplicationDeviceActivation{}

      iex> get_application_device_activation!(456)
      ** (Ecto.NoResultsError)

  """
  def get_application_device_activation!(id), do: Repo.get!(ApplicationDeviceActivation, id)

  def get_application_device_activation(id), do: Repo.get(ApplicationDeviceActivation, id)

  def get_application_device_activation_by_user_code(user_code) do
    ApplicationDeviceActivation
    |> where([ada], ada.user_code == ^user_code)
    |> preload(:application)
    |> Repo.one()
  end

  def get_application_device_activation_by_device_code(client_id, device_code) do
    ApplicationDeviceActivation
    |> join(:left, [ada], a in assoc(ada, :application))
    |> where([_, a], a.client_id == ^client_id)
    |> where([ada], ada.device_code == ^device_code)
    |> select([ada], ada)
    |> Repo.one()
  end

  @doc """
  Creates a application_device_activation.

  ## Examples

      iex> create_application_device_activation(%{field: value})
      {:ok, %ApplicationDeviceActivation{}}

      iex> create_application_device_activation(%{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def create_application_device_activation(attrs \\ %{}) do
    %ApplicationDeviceActivation{}
    |> ApplicationDeviceActivation.changeset(attrs)
    |> Repo.insert()
  end

  @doc """
  Updates a application_device_activation.

  ## Examples

      iex> update_application_device_activation(application_device_activation, %{field: new_value})
      {:ok, %ApplicationDeviceActivation{}}

      iex> update_application_device_activation(application_device_activation, %{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def update_application_device_activation(
        %ApplicationDeviceActivation{} = application_device_activation,
        attrs
      ) do
    application_device_activation
    |> ApplicationDeviceActivation.changeset(attrs)
    |> Repo.update()
    |> case do
      {:ok, application_device_activation} ->
        {:ok, Repo.preload(application_device_activation, :application)}

      error ->
        error
    end
  end

  @doc """
  Deletes a application_device_activation.

  ## Examples

      iex> delete_application_device_activation(application_device_activation)
      {:ok, %ApplicationDeviceActivation{}}

      iex> delete_application_device_activation(application_device_activation)
      {:error, %Ecto.Changeset{}}

  """
  def delete_application_device_activation(
        %ApplicationDeviceActivation{} = application_device_activation
      ) do
    Repo.delete(application_device_activation)
  end

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

  ## Examples

      iex> change_application_device_activation(application_device_activation)
      %Ecto.Changeset{data: %ApplicationDeviceActivation{}}

  """
  def change_application_device_activation(
        %ApplicationDeviceActivation{} = application_device_activation,
        attrs \\ %{}
      ) do
    ApplicationDeviceActivation.changeset(application_device_activation, attrs)
  end

  @spec prune_old_application_device_activations(pos_integer()) :: {non_neg_integer(), nil}
  def prune_old_application_device_activations(lifetime) do
    exp = DateTime.utc_now() |> DateTime.add(-lifetime) |> DateTime.to_unix()

    ApplicationDeviceActivation
    |> where([at], at.expires_in < ^exp)
    |> Repo.delete_all()
  end
end