package wow.doge.mygame.scriptsystem

import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.scaladsl.PoolRouter
import akka.actor.typed.scaladsl.Routers
import wow.doge.mygame.state.ScriptActor
import akka.actor.typed.ActorRef
import akka.actor.typed.scaladsl.ActorContext
import akka.actor.typed.Behavior
import akka.util.Timeout
import scala.util.Success
import scala.util.Failure
import akka.actor.typed.SupervisorStrategy

object ScriptCachingActor {

  /**
    * aka script representation
    */
  type ScriptRepr = Any
  type ScriptsMap = Map[os.Path, ScriptRepr]
  type ScriptResult = Either[ScriptActor.Error, ScriptRepr]

  sealed trait Command
  final case class Get(
      scriptPath: os.Path,
      requester: ActorRef[ScriptResult]
  ) extends Command
  final case class GetMap(requester: ActorRef[ScriptsMap]) extends Command
  final case class Put(scriptPath: os.Path, script: ScriptRepr) extends Command
  private final case object NoOp extends Command

  private final case class DelegateToChild(
      scriptActor: ActorRef[ScriptActor.Command],
      scriptPath: os.Path,
      requester: ActorRef[ScriptResult]
  ) extends Command

  final case class Props(
      ctx: ActorContext[Command],
      scriptActor: ActorRef[ScriptActor.Command]
  )
  final case class State(scriptsMap: ScriptsMap)
  def apply(state: State = State(Map.empty)): Behavior[Command] =
    Behaviors.logMessages {
      Behaviors.setup { ctx =>
        val pool = ScriptActorPool(4)
        val scriptsRouter = ctx.spawn(pool, "script-actors-pool")
        new ScriptCachingActor(Props(ctx, scriptsRouter)).receiveMessage(state)
      }
    }

  private def getOrCompileScript(
      ctx: ActorContext[Command],
      scriptPath: os.Path,
      scriptsMap: ScriptsMap,
      scriptActor: ActorRef[ScriptActor.Command],
      requester: ActorRef[ScriptResult]
  ) = {
    scriptsMap
      .get(scriptPath)
      .fold {
        ctx.log.debug("Delegating to child")
        ctx.self ! DelegateToChild(
          scriptActor,
          scriptPath,
          requester
        )
      } { s =>
        ctx.log.debug("Getting script from cache")
        requester ! Right(s)
      }
  }

  private def askChildForScriptCompilation(
      ctx: ActorContext[Command],
      scriptActor: ActorRef[ScriptActor.Command],
      scriptPath: os.Path,
      requester: ActorRef[ScriptResult]
  )(implicit timeout: Timeout) = {
    ctx.ask(scriptActor, ScriptActor.CompileAny(scriptPath, _)) {
      case Success(value) =>
        requester ! value
        value.fold(
          err => {
            ctx.log.error(err.reason)
            NoOp
          },
          res => {
            Put(scriptPath, res)
          }
        )
      case Failure(exception) => {
        ctx.log.error(exception.getMessage())
        NoOp
      }
    }
  }
}

class ScriptCachingActor(props: ScriptCachingActor.Props) {
  import com.softwaremill.quicklens._
  import ScriptCachingActor._
  def receiveMessage(state: State): Behavior[Command] =
    Behaviors.receiveMessage { msg =>
      msg match {
        case Get(scriptPath, requester) =>
          getOrCompileScript(
            props.ctx,
            scriptPath,
            state.scriptsMap,
            props.scriptActor,
            requester
          )
          Behaviors.same

        case DelegateToChild(scriptActor, scriptPath, requester) =>
          import scala.concurrent.duration._
          implicit val timeout = Timeout(15.seconds)
          // child ! ScriptActor.CompileAny(scriptPath, requester)
          askChildForScriptCompilation(
            props.ctx,
            scriptActor,
            scriptPath,
            requester
          )
          Behaviors.same

        case GetMap(requester) =>
          requester ! state.scriptsMap
          Behaviors.same

        case Put(scriptPath, script) =>
          props.ctx.log.debug(s"Putting $script at path $scriptPath")
          val newState =
            state.modify(_.scriptsMap).using(_ + (scriptPath -> script))
          props.ctx.log.debug(newState.toString())
          receiveMessage(state = newState)

        case NoOp => Behaviors.same
      }
    }
}

object ScriptActorPool {
  def apply(
      poolSize: Int
  ): PoolRouter[ScriptActor.Command] =
    Routers.pool(poolSize = poolSize)(
      // make sure the workers are restarted if they fail
      Behaviors
        .supervise(ScriptActor())
        .onFailure[Exception](SupervisorStrategy.restart)
    )

  // def apply(
  //     poolSize: Int,
  //     parent: ActorRef[ScriptStoringActor.Command]
  // ): PoolRouter[ScriptActor.Command] =
  //   Routers.pool(poolSize = poolSize)(
  //     // make sure the workers are restarted if they fail
  //     Behaviors
  //       .supervise(ScriptActor(parent = parent))
  //       .onFailure[Exception](SupervisorStrategy.restart)
  //   )
}
