/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * license agreements; and to You under the Apache License, version 2.0:
 *
 *   https://www.apache.org/licenses/LICENSE-2.0
 *
 * This file is part of the Apache Pekko project, which was derived from Akka.
 */

/*
 * Copyright (C) 2017-2022 Lightbend Inc. <https://www.lightbend.com>
 */

package org.apache.pekko.persistence.typed.state.scaladsl

import scala.annotation.tailrec

import org.apache.pekko
import pekko.actor.typed.BackoffSupervisorStrategy
import pekko.actor.typed.Behavior
import pekko.actor.typed.Signal
import pekko.actor.typed.internal.BehaviorImpl.DeferredBehavior
import pekko.actor.typed.internal.InterceptorImpl
import pekko.actor.typed.internal.LoggerClass
import pekko.actor.typed.scaladsl.ActorContext
import pekko.annotation.ApiMayChange
import pekko.annotation.DoNotInherit
import pekko.persistence.typed.state.internal._
import pekko.persistence.typed.PersistenceId
import pekko.persistence.typed.SnapshotAdapter

/**
 * API May Change
 */
@ApiMayChange
object DurableStateBehavior {

  /**
   * Type alias for the command handler function that defines how to act on commands.
   *
   * The type alias is not used in API signatures because it's easier to see (in IDE) what is needed
   * when full function type is used. When defining the handler as a separate function value it can
   * be useful to use the alias for shorter type signature.
   */
  type CommandHandler[Command, State] = (State, Command) => Effect[State]

  private val logPrefixSkipList = classOf[DurableStateBehavior[_, _]].getName :: Nil

  /**
   * Create a `Behavior` for a persistent actor with durable storage of its state.
   *
   * @param persistenceId stable unique identifier for the `DurableStateBehavior`
   * @param emptyState the initial state for the entity before any state has been stored
   * @param commandHandler map commands to effects e.g. persisting state, replying to commands
   */
  def apply[Command, State](
      persistenceId: PersistenceId,
      emptyState: State,
      commandHandler: (State, Command) => Effect[State]): DurableStateBehavior[Command, State] = {
    val loggerClass = LoggerClass.detectLoggerClassFromStack(classOf[DurableStateBehavior[_, _]], logPrefixSkipList)
    DurableStateBehaviorImpl(persistenceId, emptyState, commandHandler, loggerClass)
  }

  /**
   * Create a `Behavior` for a persistent actor that is enforcing that replies to commands are not forgotten.
   * Then there will be compilation errors if the returned effect isn't a [[ReplyEffect]], which can be
   * created with [[Effect.reply]], [[Effect.noReply]], [[EffectBuilder.thenReply]], or [[EffectBuilder.thenNoReply]].
   */
  def withEnforcedReplies[Command, State](
      persistenceId: PersistenceId,
      emptyState: State,
      commandHandler: (State, Command) => ReplyEffect[State]): DurableStateBehavior[Command, State] = {
    val loggerClass = LoggerClass.detectLoggerClassFromStack(classOf[DurableStateBehavior[_, _]], logPrefixSkipList)
    DurableStateBehaviorImpl(persistenceId, emptyState, commandHandler, loggerClass)
  }

  /**
   * The `CommandHandler` defines how to act on commands. A `CommandHandler` is
   * a function:
   *
   * {{{
   *   (State, Command) => Effect[State]
   * }}}
   *
   * The [[CommandHandler#command]] is useful for simple commands that don't need the state
   * and context.
   */
  object CommandHandler {

    /**
     * Convenience for simple commands that don't need the state and context.
     *
     * @see [[Effect]] for possible effects of a command.
     */
    def command[Command, State](commandHandler: Command => Effect[State]): (State, Command) => Effect[State] =
      (_, cmd) => commandHandler(cmd)

  }

  /**
   * The last sequence number that was persisted, can only be called from inside the handlers of a `DurableStateBehavior`
   */
  def lastSequenceNumber(context: ActorContext[_]): Long = {
    @tailrec
    def extractConcreteBehavior(beh: Behavior[_]): Behavior[_] =
      beh match {
        case interceptor: InterceptorImpl[_, _] => extractConcreteBehavior(interceptor.nestedBehavior)
        case concrete                           => concrete
      }

    extractConcreteBehavior(context.currentBehavior) match {
      case w: Running.WithRevisionAccessible => w.currentRevision
      case s                                 =>
        throw new IllegalStateException(s"Cannot extract the lastSequenceNumber in state ${s.getClass.getName}")
    }
  }

}

/**
 * Further customization of the `DurableStateBehavior` can be done with the methods defined here.
 *
 * Not for user extension
 *
 * API May Change
 */
@ApiMayChange @DoNotInherit trait DurableStateBehavior[Command, State] extends DeferredBehavior[Command] {

  def persistenceId: PersistenceId

  /**
   * Allows the `DurableStateBehavior` to react on signals.
   *
   * The regular lifecycle signals can be handled as well as `DurableStateBehavior` specific signals
   * (recovery related). Those are all subtypes of [[pekko.persistence.typed.state.DurableStateSignal]]
   */
  def receiveSignal(signalHandler: PartialFunction[(State, Signal), Unit]): DurableStateBehavior[Command, State]

  /**
   * @return The currently defined signal handler or an empty handler if no custom handler previously defined
   */
  def signalHandler: PartialFunction[(State, Signal), Unit]

  /**
   * Change the `DurableStateStore` plugin id that this actor should use.
   */
  def withDurableStateStorePluginId(id: String): DurableStateBehavior[Command, State]

  /**
   * The tag that can used in persistence query
   */
  def withTag(tag: String): DurableStateBehavior[Command, State]

  /**
   * Transform the state to another type before giving to the store. Can be used to transform older
   * state types into the current state type e.g. when migrating from Persistent FSM to Typed DurableStateBehavior.
   */
  def snapshotAdapter(adapter: SnapshotAdapter[State]): DurableStateBehavior[Command, State]

  /**
   * Back off strategy for persist failures.
   *
   * Specifically BackOff to prevent resume being used. Resume is not allowed as
   * it will be unknown if the state has been persisted.
   *
   * This supervision is only around the `DurableStateBehavior` not any outer setup/withTimers
   * block. If using restart, any actions e.g. scheduling timers, can be done on the PreRestart
   *
   * If not specified the actor will be stopped on failure.
   */
  def onPersistFailure(backoffStrategy: BackoffSupervisorStrategy): DurableStateBehavior[Command, State]

  /**
   * Define a custom stash capacity per entity.
   * If not defined, the default `pekko.persistence.typed.stash-capacity` will be used.
   * @since 1.3.0
   */
  def withStashCapacity(size: Int): DurableStateBehavior[Command, State]
}
