defmodule Auth.PeopleRoles do
  @moduledoc """
  Defines people_roles schema and fuction to grant roles to a person.
  """
  use Ecto.Schema
  import Ecto.Changeset
  import Ecto.Query
  alias Auth.Repo
  # https://stackoverflow.com/a/47501059/1148249
  alias __MODULE__

  schema "people_roles" do
    belongs_to :app, Auth.App
    belongs_to :person, Auth.Person
    belongs_to :role, Auth.Role
    field :granter_id, :integer
    field :revoked, :utc_datetime
    field :revoker_id, :integer

    timestamps()
  end

  @doc """
  list_people_roles/0 returns the list of people_roles with all people data.
  This is useful for displaying the data in a admin overview table.
  """
  def list_people_roles do
    Repo.all(from pr in __MODULE__, preload: [:person, :role])
  end

  @doc """
  get_record/2 returns the record where the person was granted a role.
  """
  def get_record(person_id, role_id) do
    Repo.one(
      from(pr in __MODULE__,
        where: pr.person_id == ^person_id and pr.role_id == ^role_id,
        preload: [:person, :role]
      )
    )
  end

  @doc """
  get_by_id!/1 returns the record with the given people_roles.id.
  """
  def get_by_id(id) do
    Repo.one(
      from(pr in __MODULE__,
        where: pr.id == ^id,
        preload: [:person, :role]
      )
    )
  end

  @doc """
  get_roles_for_person/1 returns the list of roles for a given person.id
  """
  def get_roles_for_person(person_id) do
    Repo.all(
      from(pr in __MODULE__,
        where: pr.person_id == ^person_id,
        preload: [:role, :app]
      )
    )
  end

  @doc """
  get_roles_for_person_for_app/2 returns the list of roles
  for a given person.id for the specific app.
  """
  def get_roles_for_person_for_app(app_id, person_id) do
    Repo.all(
      from(pr in __MODULE__,
        where:
          pr.person_id == ^person_id and
            pr.app_id == ^app_id and
            is_nil(pr.revoked),
        preload: [:role]
      )
    )
  end

  @doc """
  `insert/4` grants a role to the given person
  app_id for app the person is granted the role for (always scoped to app!)
  grantee_id is the person.id of the person being granted the role
  granter_id is the id of the person (admin) granting the role
  role_id is the role.id (int, e.g: 4) of th role being granted.
  """
  def insert(app_id, grantee_id, granter_id, role_id) do
    %PeopleRoles{}
    |> cast(%{app_id: app_id, granter_id: granter_id}, [:app_id, :granter_id])
    |> put_assoc(:app, Auth.App.get_app!(app_id))
    |> put_assoc(:person, Auth.Person.get_person_by_id(grantee_id))
    |> put_assoc(:role, Auth.Role.get_role!(role_id))
    |> Repo.insert()
  end

  @doc """
  `upsert/4` grants a role (`role_id`) to the given person (`grantee_id`)
  for the `app_id`.
  `granter_id` is the id of the person (admin) granting the role.
  
  """
  def upsert(app_id, grantee_id, granter_id, role_id) do
    case get_roles_for_person_for_app(app_id, grantee_id) do
      # if there are no roles for the person, insert it:
      n when n in [nil, []] ->
        [insert(app_id, grantee_id, granter_id, role_id)]
      roles ->
        # if the role exists in the list of roles, return the list
        if Enum.find_value(roles, fn r -> r.id == role_id end) do
          roles
        else
          [insert(app_id, grantee_id, granter_id, role_id)]
        end
    end
  end


  @doc """
  `revoke/3` grants a role to the given person
  revoker_id is the id of the person (admin) granting the role
  person_id is the person.id of the person being granted the role
  role_id is the role.id (int, e.g: 4) of th role being granted.
  """
  def revoke(revoker_id, people_roles_id) do
    # get the people_role record that needs to be updated (revoked)
    get_by_id(people_roles_id)
    |> cast(
      %{revoker_id: revoker_id, revoked: DateTime.utc_now()},
      [:revoker_id, :revoked]
    )
    |> Repo.update()
  end
end
