package ch.epfl.bluebrain.nexus.kg.cache

import akka.actor.ActorSystem
import cats.Monad
import cats.effect.{Effect, Timer}
import cats.implicits._
import ch.epfl.bluebrain.nexus.commons.cache.KeyValueStore
import ch.epfl.bluebrain.nexus.iam.client.IamClient
import ch.epfl.bluebrain.nexus.iam.client.types.events.Event._
import ch.epfl.bluebrain.nexus.iam.client.types.{AccessControlList, AccessControlLists, ResourceAccessControlList}
import ch.epfl.bluebrain.nexus.kg.config.AppConfig
import ch.epfl.bluebrain.nexus.kg.config.AppConfig._
import ch.epfl.bluebrain.nexus.kg.config.Vocabulary._
import ch.epfl.bluebrain.nexus.rdf.Iri.Path

/**
  * The acl cache backed by a KeyValueStore using akka Distributed Data
  *
  * @param store the underlying Distributed Data LWWMap store.
  */
class AclsCache[F[_]] private (store: KeyValueStore[F, Path, ResourceAccessControlList])(implicit F: Monad[F])
    extends Cache[F, Path, ResourceAccessControlList](store) {

  /**
    * Fetches all the ACLs
    */
  def list: F[AccessControlLists] =
    store.entries.map(AccessControlLists.apply)

  /**
    * Appends the provided ACLs to the existing ACLs on the provided ''path''.
    *
    * @param path the path
    * @param acl  the acls to append on the provided path
    */
  def append(path: Path, acl: ResourceAccessControlList): F[Unit] =
    get(path).flatMap {
      case Some(curr) =>
        replace(path, acl.copy(value = curr.value ++ acl.value, createdBy = curr.createdBy, createdAt = curr.createdAt))
      case _ =>
        replace(path, acl)
    }

  /**
    * Subtracts the provided ACLs from the existing ACLs on the provided ''path''.
    *
    * @param path the path
    * @param acl  the acls to subtract from the provided path
    */
  def subtract(path: Path, acl: ResourceAccessControlList): F[Unit] =
    get(path).flatMap {
      case Some(curr) =>
        replace(
          path,
          acl.copy(value = subtract(acl.value, curr.value), createdBy = curr.createdBy, createdAt = curr.createdAt)
        )
      case _ =>
        F.unit
    }

  /**
    * Deletes a path from the store.
    *
    * @param path the path to be deleted from the store
    */
  def remove(path: Path): F[Unit] = store.remove(path)

  private def subtract(acl: AccessControlList, curr: AccessControlList): AccessControlList =
    AccessControlList(acl.value.foldLeft(curr.value) {
      case (acc, (identity, permsToSubtract)) =>
        acc.get(identity).map(_ -- permsToSubtract) match {
          case Some(remaining) if remaining.isEmpty => acc - identity
          case Some(remaining)                      => acc + (identity -> remaining)
          case None                                 => acc
        }
    })
}

object AclsCache {

  private def toResourceAcl(event: AclEvent, acl: AccessControlList)(
      implicit iamConfig: IamConfig
  ): ResourceAccessControlList =
    ResourceAccessControlList(
      iamConfig.iamClient.aclsIri + event.path,
      event.rev,
      Set(nxv.AccessControlList.value),
      event.instant,
      event.subject,
      event.instant,
      event.subject,
      acl
    )

  /**
    * Creates a new acl index.
    */
  def apply[F[_]: Effect: Timer](
      iamClient: IamClient[F]
  )(implicit as: ActorSystem, config: AppConfig): AclsCache[F] = {
    val cache = new AclsCache(KeyValueStore.distributed("acls", (_, acls) => acls.rev))
    val handle: AclEvent => F[Unit] = {
      case event: AclReplaced   => cache.replace(event.path, toResourceAcl(event, event.acl))
      case event: AclAppended   => cache.append(event.path, toResourceAcl(event, event.acl))
      case event: AclSubtracted => cache.subtract(event.path, toResourceAcl(event, event.acl))
      case event: AclDeleted    => cache.remove(event.path)
    }
    iamClient.aclEvents(handle)(config.iam.serviceAccountToken)
    cache
  }
}