defmodule Glific.AccessControl.UserRole do
  @moduledoc """
  A pipe for managing the user roles
  """

  alias Glific.{
    AccessControl.Role,
    Partners.Organization,
    Repo,
    Users.User
  }

  alias __MODULE__

  use Ecto.Schema
  import Ecto.Changeset

  @required_fields [:user_id, :role_id, :organization_id]

  @type t() :: %__MODULE__{
          __meta__: Ecto.Schema.Metadata.t(),
          id: non_neg_integer | nil,
          user: User.t() | Ecto.Association.NotLoaded.t() | nil,
          role: Role.t() | Ecto.Association.NotLoaded.t() | nil,
          organization_id: non_neg_integer | nil,
          organization: Organization.t() | Ecto.Association.NotLoaded.t() | nil
        }

  schema "user_roles" do
    belongs_to(:user, User)
    belongs_to(:role, Role)
    belongs_to(:organization, Organization)
  end

  @doc """
  Standard changeset pattern we use for all data types
  """
  @spec changeset(UserRole.t(), map()) :: Ecto.Changeset.t()
  def changeset(user, attrs) do
    user
    |> cast(attrs, @required_fields)
    |> validate_required(@required_fields)
    |> unique_constraint([:user_id, :role_id])
    |> foreign_key_constraint(:user_id)
    |> foreign_key_constraint(:role_id)
  end

  @doc """
  Creates a access control.
  ## Examples
      iex> create_user_role(%{field: value})
      {:ok, %UserRole{}}
      iex> create_user_role(%{field: bad_value})
      {:error, %Ecto.Changeset{}}
  """
  @spec create_user_role(map()) :: {:ok, UserRole.t()} | {:error, Ecto.Changeset.t()}
  def create_user_role(attrs \\ %{}) do
    %UserRole{}
    |> changeset(attrs)
    |> Repo.insert()
  end

  @doc """
  Update user roles based on add_role_ids and delete_role_ids and return number_deleted as integer and roles added as access_controls
  """
  @spec update_user_roles(map()) :: map()
  def update_user_roles(
        %{
          user_id: user_id,
          add_role_ids: add_role_ids
        } = attrs
      ) do
    access_controls =
      Enum.reduce(
        add_role_ids,
        [],
        fn role_id, acc ->
          case create_user_role(Map.merge(attrs, %{role_id: role_id, user_id: user_id})) do
            {:ok, access_control} -> [access_control | acc]
            _ -> acc
          end
        end
      )

    {number_deleted, _} =
      if Map.has_key?(attrs, :delete_role_ids),
        do: delete_user_roles_by_role_ids(user_id, attrs.delete_role_ids),
        else: {0, attrs}

    %{
      number_deleted: number_deleted,
      access_controls: access_controls
    }
  end

  @doc """
  Delete user roles
  """
  @spec delete_user_roles_by_role_ids(integer, list()) :: {integer(), nil | [term()]}
  def delete_user_roles_by_role_ids(user_id, role_ids) do
    fields = {{:user_id, user_id}, {:role_id, role_ids}}
    Repo.delete_relationships_by_ids(UserRole, fields)
  end
end
