package ru.yandex.atom.service

import ru.yandex.atom.db.cassandra.CassandraActorComponent
import akka.actor.{ActorRef, Actor, Props}
import scala.collection.immutable.Queue
import ru.yandex.atom.utils.actor.AtomActorLogging
import scala.concurrent.duration.{FiniteDuration, Deadline}
import com.typesafe.config.Config
import ru.yandex.atom.utils.config._
import ru.yandex.atom.error.InternalProblem

/**
 * @author avhaliullin
 */
trait UrlListsActorComponentImpl extends UrlListsActorComponent with UrlListsWorkerActorComponentImpl {
  component: CassandraActorComponent
    with IdGeneratorActorComponent
    with UrlNormalizerActorComponent =>

  class UrlListsActorImpl(config: UrlListActorConfig) extends Actor with AtomActorLogging {

    import UrlListsResponse._

    case class WaitingRequest(req: UrlListsRequest, client: ActorRef, deadline: Deadline)

    def spawnChild: ActorRef = context.actorOf(UrlListsWorkerActor.props(config.workerConfig))

    for (i <- 0 until config.minWorkers) {
      spawnChild
    }

    /**
     * Любой Receive обязуется в конце делать become
     */
    def receive = {
      case req: UrlListsRequest =>
        working(context.children.head, context.children.toSet, Map())(req)
    }

    /**
     * Разгребаем очередь сообщений
     */
    def queueing(queue: Queue[WaitingRequest], worker2Client: Map[ActorRef, ActorRef]): Receive = {
      case req: UrlListsRequest =>
        val newQueue = removeExpired(queue)

        if (newQueue.size < config.maxQueueSize) {
          log.warning( "Enqueue request {}, queue size {}", req, newQueue.size)
          context.become(queueing(queue enqueue WaitingRequest(req, sender, config.queueTimeout fromNow), worker2Client))
        } else {
          log.error( "Cannot serve request, queue is full")
          sender ! InternalProblemResponse(req, InternalProblem.TemporarilyProblem)
        }
      case res: UrlListsResponse if worker2Client.contains(sender) =>
        val worker = sender
        val client = worker2Client(worker)
        client ! res
        if (queue.isEmpty) {
          context.become(working(worker, Set(worker), worker2Client - worker))
        } else {
          val (req, newQueue) = queue.dequeue
          worker ! req.req
          context.become(queueing(newQueue, worker2Client + (worker -> req.client)))
        }
    }

    /**
     * Обрабатываем сообщения в нормальном режиме
     */
    def working(nextWorker: ActorRef, freeWorkers: Set[ActorRef], worker2Client: Map[ActorRef, ActorRef]): Receive = {
      case req: UrlListsRequest =>
        nextWorker ! req
        tryExtend(freeWorkers - nextWorker, worker2Client + (nextWorker -> sender))
      case res: UrlListsResponse if worker2Client.contains(sender) =>
        val child = sender
        val client = worker2Client(child)
        client ! res
        tryShrink(freeWorkers + child, worker2Client - child)
    }


    def removeExpired(queue: Queue[WaitingRequest]): Queue[WaitingRequest] = {
      val (dead, rest) = queue.span(_.deadline.isOverdue())
      dead.foreach {
        req =>
          logWithId.error(req.req.id, "Expired request {}", req)
          req.client ! InternalProblemResponse(req.req, InternalProblem.TemporarilyProblem)
      }
      rest
    }

    /**
     * Магии нет, масштабируемся как ArrayList
     */
    def tryShrink(freeWorkers: Set[ActorRef], worker2Client: Map[ActorRef, ActorRef]) = {
      val totalWorkersCount = context.children.size
      val freeWorkersCount = freeWorkers.size
      val busyWorkersCount = totalWorkersCount - freeWorkersCount

      val toKillCount = math.min(totalWorkersCount - config.minWorkers, totalWorkersCount / 2)
      val newFreeWorkers = if (toKillCount > 0 &&
        busyWorkersCount <= totalWorkersCount / 4) {
        val (toKill, rest) = freeWorkers.splitAt(toKillCount)
        toKill.foreach(context.stop)
        rest
      } else freeWorkers

      val nextWorker = newFreeWorkers.head
      context.become(working(nextWorker, newFreeWorkers, worker2Client))
    }

    def tryExtend(freeWorkers: Set[ActorRef], worker2Client: Map[ActorRef, ActorRef]) = {
      val totalWorkersCount = context.children.size
      val freeWorkersCount = freeWorkers.size

      val toSpawnCount = math.min(config.maxWorkers - totalWorkersCount, totalWorkersCount)
      val newFreeWorkers = if (toSpawnCount > 0 && freeWorkersCount == 0) {
        val newWorkers = for (i <- 0 until toSpawnCount) yield {
          spawnChild
        }
        freeWorkers ++ newWorkers
      } else freeWorkers
      newFreeWorkers.headOption match {
        case Some(nextWorker) => context.become(working(nextWorker, newFreeWorkers, worker2Client))
        case None => context.become(queueing(Queue(), worker2Client))
      }
    }
  }

  object UrlListsActorImpl {
    def props(config: UrlListActorConfig) = Props(classOf[UrlListsActorImpl], component, config)
  }

  case class UrlListActorConfig(maxQueueSize: Int, queueTimeout: FiniteDuration, minWorkers: Int,
                                maxWorkers: Int, cassandraTimeout: FiniteDuration, idGeneratorTimeout: FiniteDuration,
                                normalizerTimeout: FiniteDuration) {
    def workerConfig = UrlListsWorkerActorConfig(cassandraTimeout, idGeneratorTimeout, normalizerTimeout)
  }

  object UrlListActorConfig {
    implicit def apply(config: Config): UrlListActorConfig = new UrlListActorConfig(
      maxQueueSize = config.getInt("maxQueueSize"),
      queueTimeout = config.getFiniteDuration("queueTimeout"),
      minWorkers = config.getInt("minWorkers"),
      maxWorkers = config.getInt("maxWorkers"),
      cassandraTimeout = config.getFiniteDuration("cassandraTimeout"),
      idGeneratorTimeout = config.getFiniteDuration("idGeneratorTimeout"),
      normalizerTimeout = config.getFiniteDuration("normalizerTimeout")
    )
  }

}
