package ru.yandex.atom.service

import ru.yandex.atom.db.cassandra.CassandraActorComponent
import akka.actor.{Props, ActorRef, Actor}
import com.datastax.driver.core.ConsistencyLevel
import scala.concurrent.duration.FiniteDuration
import ru.yandex.atom.error.UserProblem.UrlListNotFound
import ru.yandex.atom.data.{MagicNumbers, NormalizedUrl, URLList}
import ru.yandex.atom.utils.actor.AtomActorLogging
import ru.yandex.atom.error.UserProblem
import ru.yandex.atom.utils.actor.messages.{AbstractMessage, ResponseMessage, IFailureResponse}
import ru.yandex.atom.db.cassandra.querybuilder._
import language.postfixOps
import scala.collection.JavaConversions._

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

  class UrlListsWorkerActor(config: UrlListsWorkerActorConfig) extends Actor with AtomActorLogging {

    import UrlListsRequest._
    import UrlListsResponse._
    import context._

    implicit val consistencyLevel = ConsistencyLevel.LOCAL_QUORUM

    val cassandraWithTimeout = WithTimeout(cassandraActor, "cassandra", config.cassandraTimeout)
    val urlNormalizerWithTimeout = WithTimeout(urlNormalizerActor, "url normalizer", config.normalizerTimeout)
    val idGeneratorWithTimeout = WithTimeout(idGeneratorActor, "id generator", config.idGeneratorTimeout)

    sealed trait InternalMessage

    object InternalMessage {

      case class TimeoutMessage(req: AbstractMessage, timedOutComponent: String) extends InternalMessage

    }

    def receive = waitingClient

    /* Ожидание клиента */

    def waitingClient: Receive = {
      case req@ViewUrls(id, userId, listId) =>
        val cassandraRequest = cassandraWithTimeout ! CassandraRequest.ReadRequest(id,
          SELECT('host, 'main_mirror, 'urls) FROM 'url_lists WHERE ('user_id === userId) AND ('id === listId))
        become(viewUrls(sender, req, cassandraRequest))

      case req@ListIds(id, userId) =>
        val cassandraRequest = cassandraWithTimeout ! CassandraRequest.ReadRequest(id,
          SELECT('id) FROM 'url_lists WHERE ('user_id === userId))
        become(listIds(sender, req, cassandraRequest))

      case req@UpdateUrls(id, userId, listId, list) =>
        if (list.isEmpty) {
          sender ! UserProblemResponse(req, UserProblem.InvalidUrlListSize(0))
        } else if (list.size > MagicNumbers.MAX_URL_LIST_SIZE) {
          sender ! UserProblemResponse(req, UserProblem.InvalidUrlListSize(list.size))
        } else {
          val cassandraRequest = cassandraWithTimeout ! CassandraRequest.ReadRequest(id,
            SELECT COUNT * FROM 'url_lists WHERE ('user_id === userId) AND ('id === listId))
          become(updateUrls_checkExist(sender, req, cassandraRequest))
        }

      case req@CreateUrls(id, userId, urls) =>
        if (urls.isEmpty) {
          sender ! UserProblemResponse(req, UserProblem.InvalidUrlListSize(0))
        } else if (urls.size > MagicNumbers.MAX_URL_LIST_SIZE) {
          sender ! UserProblemResponse(req, UserProblem.InvalidUrlListSize(urls.size))
        } else {
          val normalizerReq = urlNormalizerWithTimeout ! UrlNormalizerRequest.Normalize(req.id, urls)
          become(createUrls_normalization(sender, req, normalizerReq))
        }
    }

    /* Запрос id'шиков списков урлов */

    def listIds(client: ActorRef, req: ListIds, waitingReq: CassandraRequest.ReadRequest): Receive =
      handleFailures(client, req, waitingReq, "list ids") {
        case CassandraResponse.ReadResponse(r, rows) if r == waitingReq =>
          finishRequest(client, ListIdsResponse(req, rows.map(_.getLong("id")).toSet))
      }

    /* Просмотр списка урлов */

    def viewUrls(client: ActorRef, req: ViewUrls, waitingReq: CassandraRequest.ReadRequest): Receive =
      handleFailures(client, req, waitingReq, "view urls") {
        case CassandraResponse.ReadResponse(r, rows) if r == waitingReq =>
          val urls = rows.flatMap {
            row =>
              val host = row.getString("host")
              val mm = row.getString("main_mirror")
              row.getSet("urls", classOf[String]).map(NormalizedUrl.importFromDB(host, mm, _)).toVector
          }.toSet
          val response = if (urls.isEmpty) {
            UserProblemResponse(req, UrlListNotFound(req.userId, req.listId))
          } else {
            ViewUrlsResponse(req, URLList(req.userId, req.listId, urls))
          }
          finishRequest(client, response)
      }

    /* Обновление списка урлов */

    // Ожидание ответа проверки существования списка
    def updateUrls_checkExist(client: ActorRef, req: UpdateUrls, waitingReq: CassandraRequest.ReadRequest): Receive =
      handleFailures(client, req, waitingReq, "update urls - check exist") {
        case CassandraResponse.ReadResponse(r, rows) if r == waitingReq =>
          if (rows.exists(_.getLong(0) > 0)) {
            val normalizerReq = urlNormalizerWithTimeout ! UrlNormalizerRequest.Normalize(req.id, req.list)
            become(updateUrls_normalization(client, req, normalizerReq))
          } else {
            finishRequest(client, UserProblemResponse(req, UserProblem.UrlListNotFound(req.userId, req.listId)))
          }
      }

    //Ожидание ответа нормализатора
    def updateUrls_normalization(client: ActorRef, req: UpdateUrls, waitingReq: UrlNormalizerRequest): Receive =
      handleFailures(client, req, waitingReq, "update urls - normalization") {
        case UrlNormalizerResponse.NormalizeResponse(r, urls) if r == waitingReq =>
          val deleteTimestamp = System.currentTimeMillis() * 1000
          val insertTimestamp = deleteTimestamp + 1

          val cassandraRequest = cassandraWithTimeout ! CassandraRequest.WriteRequest(req.id,
            BATCH(
              DELETE FROM 'url_lists USING TIMESTAMP(deleteTimestamp) WHERE ('user_id === req.userId) AND ('id === req.listId),
              urlInserts(req.userId, req.listId, urls, Some(insertTimestamp)): _*
            )
          )
          context.become(updateUrls_update(client, req, cassandraRequest))
      }

    //Ожидание подтверждения обновления списка в БД
    def updateUrls_update(client: ActorRef, req: UpdateUrls, waitingReq: CassandraRequest.WriteRequest): Receive =
      handleFailures(client, req, waitingReq, "update urls - DB update") {
        case CassandraResponse.WriteResponse(r) if r == waitingReq =>
          finishRequest(client, UpdateUrlsResponse(req))
      }

    /* Создание списка */

    //Ожидание нормализатора урлов
    def createUrls_normalization(client: ActorRef, req: CreateUrls, waitingReq: UrlNormalizerRequest): Receive =
      handleFailures(client, req, waitingReq, "create urls - normalization") {
        case UrlNormalizerResponse.NormalizeResponse(r, urls) if r == waitingReq =>
          val idGeneratorRequest = idGeneratorWithTimeout ! IdGeneratorRequest.NextId(req.id)
          context.become(createUrls_generateId(client, req, urls, idGeneratorRequest))
      }

    //Ожидание генератора уникальных ID

    def createUrls_generateId(client: ActorRef, req: CreateUrls, normalizedUrls: Set[NormalizedUrl],
                              waitingReq: IdGeneratorRequest.NextId): Receive =
      handleFailures(client, req, waitingReq, "create urls - generate id") {
        case IdGeneratorResponse.NextIdResponse(r, listId) if r == waitingReq =>
          val cassandraRequest = cassandraWithTimeout ! CassandraRequest.WriteRequest(req.id,
            BATCH(
              urlInserts(req.userId, listId, normalizedUrls, None)
            )
          )
          become(createUrls_create(client, req, listId, cassandraRequest))
      }

    //Ожидание подтверждения создания списка в БД

    def createUrls_create(client: ActorRef, req: CreateUrls, listId: Long, waitingReq: CassandraRequest.WriteRequest): Receive =
      handleFailures(client, req, waitingReq, "create urls - create") {
        case CassandraResponse.WriteResponse(r) if r == waitingReq =>
          finishRequest(client, CreateUrlsResponse(req, listId))
      }

    // Общий обработчик возможных ошибок
    def handleFailures(client: ActorRef, req: UrlListsRequest, waitingToken: AbstractMessage, operationDesc: String)(handle: Receive): Receive = handle.orElse {
      case x: ResponseMessage[_] with IFailureResponse if x.request == waitingToken =>
        finishRequest(client, FailureResponse(req, x))
      case InternalMessage.TimeoutMessage(r, componentName) if r == waitingToken =>
        logWithId.error(req.id, s"Operation '$operationDesc' failed due to $componentName timeout")
    }

    /* Вспомогательные методы */

    case class WithTimeout(to: ActorRef, componentName: String, timeout: FiniteDuration) {
      def ![T <: AbstractMessage](req: T): T = {
        to ! req
        context.system.scheduler.scheduleOnce(timeout, self, InternalMessage.TimeoutMessage(req, componentName))
        req
      }
    }

    def finishRequest(client: ActorRef, response: UrlListsResponse) {
      client ! response
      become(waitingClient)
    }

    def urlInserts(userId: Long, listId: Long, urls: Set[NormalizedUrl], timestamp: Option[Long]): Seq[InsertStatement] =
      urls.groupBy(x => (x.originalHost, x.mainMirror)).map {
        it =>
          val ((host, mainMirror), normalizedUrls) = it
          INSERT INTO 'url_lists VALUES('user_id -> userId, 'id -> listId, 'host -> host, 'main_mirror -> mainMirror,
            'urls -> normalizedUrls.map(_.exportRelPart)) USING (timestamp.map(TIMESTAMP).toSeq: _*): InsertStatement
      }.toSeq

  }

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

  case class UrlListsWorkerActorConfig(cassandraTimeout: FiniteDuration,
                                       idGeneratorTimeout: FiniteDuration, normalizerTimeout: FiniteDuration)

}
