package ch.epfl.bluebrain.nexus.kg.directives

import akka.http.scaladsl.model.headers.OAuth2BearerToken
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.directives.FutureDirectives.onComplete
import akka.http.scaladsl.server.{Directive0, Directive1}
import ch.epfl.bluebrain.nexus.admin.client.types.Project
import ch.epfl.bluebrain.nexus.iam.client.types._
import ch.epfl.bluebrain.nexus.iam.client.{IamClient, IamClientError}
import ch.epfl.bluebrain.nexus.kg.KgError.{AuthenticationFailed, AuthorizationFailed, InternalError}
import ch.epfl.bluebrain.nexus.kg.resources.syntax._
import com.typesafe.scalalogging.Logger
import monix.eval.Task
import monix.execution.Scheduler.Implicits.global

import scala.util.{Failure, Success}

object AuthDirectives {

  private val logger = Logger[this.type]

  /**
    * Extracts the credentials from the HTTP Authorization Header and builds the [[AuthToken]]
    */
  def extractToken: Directive1[Option[AuthToken]] =
    extractCredentials.flatMap {
      case Some(OAuth2BearerToken(value)) => provide(Some(AuthToken(value)))
      case Some(_)                        => failWith(AuthenticationFailed)
      case _                              => provide(None)
    }

  /**
    * Checks if the current caller has the required permission.
    *
    * @param perm the permission to check on the current project
    * @return pass if the ''perm'' is present on the current project, fail with [[AuthorizationFailed]] otherwise
    */
  def hasPermission(perm: Permission)(implicit acls: AccessControlLists, caller: Caller, project: Project): Directive0 =
    if (acls.exists(caller.identities, project.projectLabel, perm)) pass
    else failWith(AuthorizationFailed)

  /**
    * Checks if the current caller has the required permission.
    *
    * @param perm     the permission to check on the current organization
    * @param orgLabel the organization label
    * @return pass if the ''perm'' is present on the current project, fail with [[AuthorizationFailed]] otherwise
    */
  def hasPermission(perm: Permission, orgLabel: String)(implicit acls: AccessControlLists, caller: Caller): Directive0 =
    if (acls.exists(caller.identities, orgLabel, perm)) pass
    else failWith(AuthorizationFailed)

  /**
    * Checks if the current caller has the required permissions on `/`
    *
    * @param  perms the permissions to check on `/`
    * @return pass if the ''perms'' are present on `/`, fail with [[AuthorizationFailed]] otherwise
    */
  def hasPermissionOnRoot(perms: Permission)(
      implicit acls: AccessControlLists,
      caller: Caller
  ): Directive0 =
    if (acls.existsOnRoot(caller.identities, perms)) pass
    else failWith(AuthorizationFailed)

  /**
    * Retrieves the caller ACLs.
    */
  def extractCallerAcls(implicit iam: IamClient[Task], token: Option[AuthToken]): Directive1[AccessControlLists] = {
    import ch.epfl.bluebrain.nexus.rdf.Iri.Path._
    onComplete(iam.acls("*" / "*", ancestors = true, self = true).runToFuture).flatMap {
      case Success(result)                         => provide(result)
      case Failure(_: IamClientError.Unauthorized) => failWith(AuthenticationFailed)
      case Failure(_: IamClientError.Forbidden)    => failWith(AuthorizationFailed)
      case Failure(err) =>
        val message = "Error when trying to check for permissions"
        logger.error(message, err)
        failWith(InternalError(message))
    }
  }

  /**
    * Authenticates the requested with the provided ''token'' and returns the ''caller''
    */
  def extractCaller(implicit iam: IamClient[Task], token: Option[AuthToken]): Directive1[Caller] =
    onComplete(iam.identities.runToFuture).flatMap {
      case Success(caller)                         => provide(caller)
      case Failure(_: IamClientError.Unauthorized) => failWith(AuthenticationFailed)
      case Failure(_: IamClientError.Forbidden)    => failWith(AuthorizationFailed)
      case Failure(err) =>
        val message = "Error when trying to extract the subject"
        logger.error(message, err)
        failWith(InternalError(message))
    }
}