package ch.epfl.bluebrain.nexus.kg.archives

import akka.actor.{ActorSystem, NotInfluenceReceiveTimeout}
import cats.Monad
import cats.data.OptionT
import cats.effect.{Effect, Timer}
import cats.implicits._
import ch.epfl.bluebrain.nexus.kg.archives.ArchiveCache._
import ch.epfl.bluebrain.nexus.kg.config.AppConfig._
import ch.epfl.bluebrain.nexus.kg.resources.ResId
import ch.epfl.bluebrain.nexus.sourcing.StateMachine
import ch.epfl.bluebrain.nexus.sourcing.akka.StopStrategy
import ch.epfl.bluebrain.nexus.sourcing.akka.statemachine.AkkaStateMachine
import retry.RetryPolicy

class ArchiveCache[F[_]: Monad](ref: StateMachine[F, String, State, Command, Unit]) {

  /**
    * Retrieves the [[Archive]] from the cache.
    *
    * @param resId the resource id for which imports are looked up
    * @return Some(archive) when found on the cache, None otherwise wrapped on the F[_] type
    */
  def get(resId: ResId): OptionT[F, Archive] =
    OptionT(ref.currentState(resId.show))

  /**
    * Writes the passed [[Archive]] to the cache. It will be invalidated after a time configured
    * at ''cfg.cache.invalidation.lapsedSinceLastInteraction'' where cfg is [[ArchivesConfig]].
    *
    * @param value the archive to write
    * @return Some(archive) when successfully added to the cache, None if it already existed in the cache wrapped on the F[_] type
    */
  def put(value: Archive): OptionT[F, Archive] =
    OptionT(ref.evaluate(value.resId.show, Write(value)).map(_.toOption.flatten))

}

object ArchiveCache {

  private[archives] type State   = Option[Archive]
  private[archives] type Command = Write
  private[archives] final case class Write(bundle: Archive) extends NotInfluenceReceiveTimeout

  final def apply[F[_]: Timer](implicit as: ActorSystem, cfg: ArchivesConfig, F: Effect[F]): F[ArchiveCache[F]] = {
    implicit val retryPolicy: RetryPolicy[F] = cfg.cache.retry.retryPolicy[F]
    val invalidationStrategy                 = StopStrategy.lapsedSinceLastInteraction[State, Command](cfg.cacheInvalidateAfter)

    val evaluate: (State, Command) => F[Either[Unit, State]] = {
      case (None, Write(bundle)) => F.pure(Right(Some(bundle)))
      case (Some(_), _)          => F.pure(Left(())) // It already exists, so we don't want to replace it
    }

    AkkaStateMachine
      .sharded[F]("archives", None, evaluate, invalidationStrategy, cfg.cache.akkaStateMachineConfig, cfg.cache.shards)
      .map(new ArchiveCache[F](_))
  }
}