package ru.yandex.atom.periodic

import ru.yandex.atom.db.cassandra.CassandraComponent
import ru.yandex.atom.zookeeper.ZookeeperActorComponent
import ru.yandex.atom.service.GeminiActorComponent
import java.io.File
import ru.yandex.atom.utils.log.AtomLogger
import ru.yandex.atom.data.{NormalizedUrl, ReqID}
import ru.yandex.atom.data.GeminiRequestType.MIRROR
import akka.pattern.ask
import ru.yandex.atom.error.ErrorUtils
import concurrent.duration._
import language.postfixOps
import org.apache.zookeeper.CreateMode
import org.apache.zookeeper.KeeperException.Code
import akka.util.Timeout
import java.util.concurrent.Executors
import com.typesafe.config.Config
import concurrent._
import ru.yandex.atom.periodic.clean.{Task, CleanerComponent}

/**
 * @author avhaliullin
 */
trait MainMirrorUpdaterComponent extends ZookeeperSyncUtils {
  component: CassandraComponent
    with ZookeeperActorComponent
    with GeminiActorComponent
    with CleanerComponent =>

  def mainMirrorUpdater: MainMirrorUpdater

  class MainMirrorUpdater(config: MainMirrorUpdaterConfig) {
    val zkPath = "/main-mirrors"
    val zkLockPath = zkPath + "/lock"
    val fileNameRegex = """update\.(\d+)""".r
    val log = AtomLogger[MainMirrorUpdater]()
    implicit val executor = ExecutionContext.fromExecutorService(Executors.newFixedThreadPool(5))

    case class Host(uid: Long, listId: Long, host: String, mainMirror: String)

    def obtainLockAndUpdateMirrors() {
      future {
        val now = System.currentTimeMillis()
        val id = ReqID("mirror updater", now.toString)

        try {
          implicit val zkTimeout: Timeout = 10.seconds

          log.info(id, "Trying to update main mirrors")

          ZK.createRecover(id, zkPath, ZK.serializeLong(0L), CreateMode.PERSISTENT, () => ()) {
            case Code.NODEEXISTS =>
          }

          val (version, lastUpdateTs) = ZK.read(id, zkPath, (stat, data) => (stat.version, ZK.deserializeLong(data)))
          log.info(id, "Last update timestamp = {}", lastUpdateTs)

          val shareDir = new File(config.shareDir)
          if (!shareDir.exists()) {
            shareDir.mkdir()
          }
          val newTimestamps = shareDir.list().collect {
            case fileNameRegex(tsString) => tsString.toLong
          }.filter(_ > lastUpdateTs)
          if (!newTimestamps.isEmpty) {
            val lastTs = newTimestamps.max
            log.info(id, "Found new main mirrors update tag with timestamp {}", lastTs)

            val obtainedLock = ZK.createRecover(id, zkLockPath, ZK.serializeString(config.hostName), CreateMode.EPHEMERAL, () => true) {
              case Code.NODEEXISTS => false
            }

            if (obtainedLock) {
              try {
                updateMirrors(id)
                ZK.update(id, zkPath, ZK.serializeLong(lastTs), version, _ => ())
                log.info(id, "Successfully updated main mirrors")
              } finally {
                ZK.delete(id, zkLockPath, 0)
              }
            } else {
              log.info(id, "Failed to obtain lock")
            }
          } else {
            log.info(id, "Main mirrors base wasn't updated")
          }
        } catch {
          case t: Throwable =>
            log.error(id, t, "Main mirrors updater failed")
        }
      }
    }

    private def updateMirrors(id: ReqID) {
      import ru.yandex.atom.db.cassandra.querybuilder._

      log.info(id, "Updating mirrors")

      cassandra.select(SELECT('user_id, 'id, 'host, 'main_mirror) FROM 'url_lists).flatMap {
        iter =>
          iter.foldLeft(()) {
            (acc, it) =>
              val hosts = it.map {
                row =>
                  val uid = row.getLong("user_id")
                  val id = row.getLong("id")
                  val host = row.getString("host")
                  val mm = row.getString("main_mirror")
                  Host(uid, id, host, mm)
              }.groupBy(_.host)
              implicit val timeout: Timeout = 1 minute

              log.info(id, "Asking gemini for {} hosts", hosts.size)
              val geminiResp = Await.result(ErrorUtils.safeMapFutureMessage[GeminiResponse, GeminiResponse.Answer](
                geminiActor ? GeminiRequest.Ask(id, MIRROR, hosts.keySet)), timeout.duration)

              log.info(id, "Gemini answered, preparing update statements")

              val toUpdate = geminiResp.canonizedUrls.flatMap {
                case (host, newMirror) =>
                  val cleanedNewMirror = NormalizedUrl.cleanHost(newMirror)
                  hosts(host).collect {
                    case hostInfo if hostInfo.mainMirror != cleanedNewMirror => hostInfo.copy(mainMirror = cleanedNewMirror )
                  }
              }.toSet

              val updates = toUpdate.map {
                host =>
                  UPDATE('url_lists) SET ('main_mirror := host.mainMirror) WHERE
                    ('user_id === host.uid) AND ('id === host.listId) AND ('host === host.host)
              }.toSeq

              log.info(id, "Executing {} updates in batch", updates.size)

              Await.result(cassandra.update(BATCH(updates)), 1 minute)

              log.info(id, "Mirrors batch updated")
          }
      }
    }

    def cleanBefore = System.currentTimeMillis() - 1000 * 60 * 60 * 24

    scheduleCleaning(Task.cleanDir(log, "cleaning main mirrors update tags", config.shareDir, _.lastModified() < cleanBefore))
  }

  case class MainMirrorUpdaterConfig(shareDir: String, hostName: String)

  object MainMirrorUpdaterConfig {
    implicit def apply(config: Config): MainMirrorUpdaterConfig = new MainMirrorUpdaterConfig(
      shareDir = config.getString("shareDir"),
      hostName = config.getString("hostName")
    )
  }

}
