package ru.yandex.featurer.service

import java.io.ByteArrayInputStream
import java.nio.charset.Charset

import com.typesafe.config.Config
import org.slf4j.LoggerFactory
import ru.yandex.featurer.data._
import ru.yandex.featurer.error.UserException

import scala.sys.process.{Process, ProcessLogger}

/**
  * @author avhaliullin
  */
trait ContainerServiceComponent {
  self: TeamcityServiceComponent =>

  def containerService: ContainerService

  class ContainerService(config: ContainerService.ContainerServiceConf, teamcityConf: TeamcityService.TeamcityServiceConfig) {
    private val log = LoggerFactory.getLogger(getClass)
    private val pl = ProcessLogger(log.info(_), log.warn(_))
    private val recordPattern = """^([^:]+):\s+(.+)$""".r
    private val containerNamePattern = "^featurer_([^_]+)_(.+)$".r

    implicit def string2Arg(s: String): Arg = Arg(s, s)

    private def exec(command: String, args: Arg*) = {
      log.info("Executing `" + command + " " + args.map(_.log).mkString(" ") + "`")
      val code = Process(command, args.map(_.console)) ! pl
      if (code != 0) {
        throw new RuntimeException("Process exited with nonzero status code " + code)
      }
    }

    private def writeFileInContainer(containerName: String, fileName: String, content: String): Unit = {
      log.info(s"Writing in container $containerName into file $fileName content:\n$content")
      val is = new ByteArrayInputStream(content.getBytes(Charset.forName("UTF-8")))
      val code = Process("sudo", Seq("lxc-attach", "-n", containerName, "--", "cp", "/dev/stdin", fileName)) #< is ! pl
      if (code != 0) {
        throw new RuntimeException("Process exited with nonzero status code " + code)
      }
    }

    private def sudo(command: String, args: Arg*) = {
      log.info("Executing with sudo `" + command + " " + args.map(_.log).mkString(" ") + "`")
      val code = Process("sudo", command :: args.map(_.console).toList) ! pl
      if (code != 0) {
        throw new RuntimeException("Process exited with nonzero status code " + code)
      }
    }

    private def requiredParam(map: Map[String, String], key: String) = {
      map.getOrElse(key, throw new RuntimeException(s"Key $key not present in lxc-info output"))
    }

    private def makeContainerName(appId: AppId, featureId: FeatureId) = "featurer_" + appId.id + "_" + featureId.id

    private def containerDir(containerName: String) = config.containersRoot + containerName

    def getContainerInfo(appId: AppId, featureId: FeatureId): Option[ContainerInfo] = {
      val containerName = makeContainerName(appId, featureId)
      var lines = Vector[String]()
      val exitCode = Process("sudo", Seq("lxc-info", "-n", containerName)) ! ProcessLogger(line => lines = lines :+ line, log.info(_))
      exitCode match {
        case 0 =>
          val infoMap = lines.foldLeft(Map[String, String]()) {
            case (acc, recordPattern(key, value)) => acc + (key.toLowerCase -> value)
            case (acc, other) => acc
          }
          val name = requiredParam(infoMap, "name")
          val stateStr = requiredParam(infoMap, "state")
          val ip = infoMap.get("ip")
          Some(ContainerInfo(appId, featureId, ContainerStateEnum.enumResolver.byName(stateStr).get, ip, name))
        case 1 => None
        case other => throw new RuntimeException("process exit with status " + other)
      }
    }

    def createAndStart(appInfo: ApplicationInfo, featureId: FeatureId): String = {
      val appId = appInfo.appId
      val info = getContainerInfo(appId, featureId) match {
        case None =>
          createContainer(appInfo, featureId)
          getContainerInfo(appId, featureId)
            .getOrElse(throw new RuntimeException(s"Just created container $appId, $featureId - not found"))
        case Some(cInfo) => cInfo
      }
      info.state match {
        case ContainerStateEnum.RUNNING =>
        case ContainerStateEnum.STARTING =>
        case ContainerStateEnum.STOPPED =>
          startContainer(appId, featureId)
        case _ => throw new RuntimeException(s"Container $info is in bad state")
      }
      getIp(appId, featureId)
    }

    private def initDNSSettings(appId: AppId, featureId: FeatureId): Unit = {
      val containerName = makeContainerName(appId, featureId)
      val fqdn = featureId.id.toLowerCase() + "." + config.dnsZoneName
      writeFileInContainer(containerName, "/etc/hostname", fqdn)
      writeFileInContainer(containerName, "/etc/hosts",
        s"""
           |127.0.0.1   localhost
           |127.0.1.1   $fqdn
           |::1     ip6-localhost ip6-loopback
           |fe00::0 ip6-localnet
           |ff00::0 ip6-mcastprefix
           |ff02::1 ip6-allnodes
           |ff02::2 ip6-allrouters
         """.stripMargin)
    }

    private def createContainer(appInfo: ApplicationInfo, featureId: FeatureId): Unit = {
      val containerName = makeContainerName(appInfo.appId, featureId)
      sudo("lxc-clone", "-o", appInfo.baseContainerName, "-n", containerName, "-B", "overlayfs", "-s")
      sudo("chmod", "a+rx", containerDir(containerName))
      sudo("lxc-start", "-n", containerName, "-d")
      initDNSSettings(appInfo.appId, featureId)
      sudo("lxc-stop", "-n", containerName)
    }

    private def getIp(appId: AppId, featureId: FeatureId): String = {
      val cName = makeContainerName(appId, featureId)
      def doGetIp = getContainerInfo(appId, featureId).getOrElse(throw UserException.ResourceNotFound("container", cName)).ip
      def getWithAttempts(count: Int): String = doGetIp match {
        case Some(result) => result
        case None => if (count > 0) {
          Thread.sleep(1000)
          getWithAttempts(count - 1)
        } else {
          throw new RuntimeException(s"Container $cName - cannot get IP")
        }
      }
      getWithAttempts(5)
    }

    private def startContainer(appId: AppId, featureId: FeatureId): Unit = {
      val containerName = makeContainerName(appId, featureId)
      sudo("lxc-start", "-n", containerName, "-d")
    }

    def destroyContainer(appId: AppId, featureId: FeatureId): Unit = {
      Process("sudo", Seq("lxc-stop", "-n", makeContainerName(appId, featureId))) ! pl
      sudo("lxc-destroy", "-n", makeContainerName(appId, featureId))
    }

    def installArtifact(appInfo: ApplicationInfo, featureId: FeatureId, artifactInfo: TeamcityArtifactInfo): String = {
      val ip = createAndStart(appInfo, featureId)
      val containerName = makeContainerName(appInfo.appId, featureId)
      sudo("lxc-attach", "-n", containerName, "--", "wget", artifactInfo.downloadUrl.toString(),
        "-O", s"/tmp/${artifactInfo.fileName}",
        "--no-check-certificate",
        Arg("**credentials**", s"--header=Authorization: ${teamcityConf.authScheme} ${teamcityConf.authToken}"))
      sudo("lxc-attach", "-n", containerName, "--", "dpkg", "-i", s"/tmp/${artifactInfo.fileName}")
      sudo("lxc-attach", "-n", containerName, "--", "ubic", "restart", appInfo.service.name)
      ip
    }

    case class Arg(log: String, console: String)

  }

  object ContainerService {

    case class ContainerServiceConf(containersRoot: String, dnsZoneName: String)

    object ContainerServiceConf {
      implicit def apply(conf: Config): ContainerServiceConf = {
        ContainerServiceConf(
          conf.getString("containersRoot"),
          conf.getString("dnsZoneName")
        )
      }
    }

  }

}
