package ru.yandex.atom.zookeeper

import java.{util => ju}

import akka.actor.{Actor, ActorRef, Props}
import com.typesafe.config.Config
import org.apache.zookeeper.AsyncCallback._
import org.apache.zookeeper.data.{ACL, Id}
import org.apache.zookeeper.{data => zoo, _}
import ru.yandex.atom.data.ReqID
import ru.yandex.atom.utils.actor.AtomActorLogging
import ru.yandex.atom.utils.config._

import scala.collection.JavaConversions._
import scala.concurrent.duration.FiniteDuration

/**
 * @author avhaliullin
 */
trait ZookeeperActorComponentImpl extends ZookeeperActorComponent {
  component =>

  import ZookeeperRequest._
  import ZookeeperResponse._

  class ZookeeperActorImpl(config: ZookeeperConfig) extends Actor with AtomActorLogging {
    private val zookeeper = new ZooKeeper(config.nodes.map(node => s"${node.host}:${node.port}").mkString(",") + s"/${config.rootPath}",
      config.sessionTimeout.toMillis.toInt, WATCHER)
    private val acl = new ACL(ZooDefs.Perms.ALL, config.aclId)
    private val aclList = new ju.ArrayList[ACL]() {
      {
        add(acl)
      }
    }

    def receive = {
      case req@UpdateRequest(id, path, data, version) =>
        val client = sender
        zookeeper.setData(path, data, version, new StatCallback {
          def processResult(rc: Int, path: String, ctx: scala.Any, stat: zoo.Stat) {
            if (ctx == req) {
              ifNoError(req, client, rc, path) {
                client ! UpdateResponse(req, Stat(stat))
              }
            }
          }
        }, req)

      case req@CreateRequest(id, path, data, mode) =>
        val client = sender
        zookeeper.create(path, data, aclList, mode, new StringCallback {
          def processResult(rc: Int, path: String, ctx: scala.Any, name: String) {
            if (ctx == req) {
              ifNoError(req, client, rc, path) {
                client ! CreateResponse(req, name)
              }
            }
          }
        }, req)

      case req@ExistsRequest(id, path) =>
        val client = sender
        zookeeper.exists(path, false, new StatCallback {
          def processResult(rc: Int, path: String, ctx: scala.Any, stat: zoo.Stat) {
            if (ctx == req) {
              ifNoError(req, client, rc, path) {
                client ! ExistsResponse(req, Stat(stat))
              }
            }
          }
        }, req)

      case req@ReadRequest(id, path) =>
        val client = sender
        zookeeper.getData(path, false, new DataCallback {
          override def processResult(rc: Int, path: String, ctx: scala.Any, data: Array[Byte], stat: zoo.Stat) {
            if (ctx == req) {
              ifNoError(req, client, rc, path) {
                client ! ReadResponse(req, data, Stat(stat))
              }
            }
          }
        }, req)

      case req@DeleteRequest(id, path, version) =>
        val client = sender
        zookeeper.delete(path, version, new VoidCallback {
          def processResult(rc: Int, path: String, ctx: scala.Any) {
            if (ctx == req) {
              ifNoError(req, client, rc, path) {
                client ! DeleteResponse(req)
              }
            }
          }
        }, req)

      case req@ChildrenRequest(id, path) =>
        val client = sender
        zookeeper.getChildren(path, false, new Children2Callback {
          override def processResult(rc: Int, path: String, ctx: scala.Any, children: ju.List[String], stat: zoo.Stat) {
            if (ctx == req) {
              ifNoError(req, client, rc, path) {
                client ! ChildrenResponse(req, children, Stat(stat))
              }
            }
          }
        }, req)
      case ZKInternalError(userReq, ex) =>
        logWithId.error(userReq.id, ex, "Unexpected zookeeper exception! Rethrowing")
        throw ex
    }

    private def ifNoError(req: ZookeeperRequest, client: ActorRef, rc: Int, path: String)(f: => Unit) = {
      if (rc == KeeperException.Code.OK.intValue()) {
        f
      } else {
        val ex = KeeperException.create(KeeperException.Code.get(rc), path)
        client ! FailureResponse(req, ex)
        if (isInternalError(rc)) {
          self ! ZKInternalError(req, ex)
        }
      }
    }

    private def isInternalError(rc: Int): Boolean = {
      import org.apache.zookeeper.KeeperException.Code._
      val code = get(rc)
      rc > APIERROR.intValue() ||
        code == NOAUTH || code == SESSIONEXPIRED || code == SESSIONMOVED || code == INVALIDACL || code == AUTHFAILED
    }

    override def postStop() = {
      super.postStop()
      zookeeper.close()
    }

    private object WATCHER extends Watcher {
      def process(event: WatchedEvent) {
        logWithId.debug(ReqID("zookeeper", event.toString), "Zookeeper event {}", event)
      }
    }

    case class ZKInternalError(req: ZookeeperRequest, ex: KeeperException)

  }

  object ZookeeperConfig {

    case class ZookeeperNode(host: String, port: Int)

    object ZookeeperNode {
      def apply(config: Config): ZookeeperNode = ZookeeperNode(
        host = config.getString("host"),
        port = if (config.hasPath("port")) config.getInt("port") else 2181
      )
    }

    implicit def apply(config: Config): ZookeeperConfig = ZookeeperConfig(
      nodes = config.getConfigList("nodes").map(ZookeeperNode.apply),
      sessionTimeout = config.getFiniteDuration("sessionTimeout"),
      rootPath = config.getString("rootPath"),
      aclId = new Id(config.getString("aclId.scheme"), config.getString("aclId.id"))
    )

  }

  case class ZookeeperConfig(nodes: Seq[ZookeeperConfig.ZookeeperNode], sessionTimeout: FiniteDuration, rootPath: String,
                             aclId: Id) {
    assume(!nodes.isEmpty, "Zookeeper should has nodes")
  }

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

}
