/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.openwhisk.core.containerpool.docker

import akka.actor.ActorSystem

import scala.concurrent.Await
import scala.concurrent.ExecutionContext
import scala.concurrent.Future
import org.apache.openwhisk.common.Logging
import org.apache.openwhisk.common.TransactionId
import org.apache.openwhisk.core.WhiskConfig
import org.apache.openwhisk.core.containerpool._
import org.apache.openwhisk.core.entity.ByteSize
import org.apache.openwhisk.core.entity.ExecManifest
import org.apache.openwhisk.core.entity.InvokerInstanceId

import scala.concurrent.duration._
import java.util.concurrent.TimeoutException

import pureconfig._
import pureconfig.generic.auto._
import org.apache.openwhisk.core.ConfigKeys

case class DockerContainerFactoryConfig(useRunc: Boolean)

class DockerContainerFactory(instance: InvokerInstanceId,
                             parameters: Map[String, Set[String]],
                             containerArgsConfig: ContainerArgsConfig =
                               loadConfigOrThrow[ContainerArgsConfig](ConfigKeys.containerArgs),
                             protected val runtimesRegistryConfig: RuntimesRegistryConfig =
                               loadConfigOrThrow[RuntimesRegistryConfig](ConfigKeys.runtimesRegistry),
                             protected val userImagesRegistryConfig: RuntimesRegistryConfig =
                               loadConfigOrThrow[RuntimesRegistryConfig](ConfigKeys.userImagesRegistry),
                             dockerContainerFactoryConfig: DockerContainerFactoryConfig =
                               loadConfigOrThrow[DockerContainerFactoryConfig](ConfigKeys.dockerContainerFactory))(
  implicit actorSystem: ActorSystem,
  ec: ExecutionContext,
  logging: Logging,
  docker: DockerApiWithFileAccess,
  runc: RuncApi)
    extends ContainerFactory {

  /** Create a container using docker cli */
  override def createContainer(tid: TransactionId,
                               name: String,
                               actionImage: ExecManifest.ImageName,
                               userProvidedImage: Boolean,
                               memory: ByteSize,
                               cpuShares: Int)(implicit config: WhiskConfig, logging: Logging): Future[Container] = {
    val registryConfig =
      ContainerFactory.resolveRegistryConfig(userProvidedImage, runtimesRegistryConfig, userImagesRegistryConfig)
    val image = if (userProvidedImage) Left(actionImage) else Right(actionImage)
    DockerContainer.create(
      tid,
      image = image,
      registryConfig = Some(registryConfig),
      memory = memory,
      cpuShares = cpuShares,
      environment = Map("__OW_API_HOST" -> config.wskApiHost) ++ containerArgsConfig.extraEnvVarMap,
      network = containerArgsConfig.network,
      dnsServers = containerArgsConfig.dnsServers,
      dnsSearch = containerArgsConfig.dnsSearch,
      dnsOptions = containerArgsConfig.dnsOptions,
      name = Some(name),
      useRunc = dockerContainerFactoryConfig.useRunc,
      parameters ++ containerArgsConfig.extraArgs.map { case (k, v) => ("--" + k, v) })
  }

  /** Perform cleanup on init */
  override def init(): Unit = removeAllActionContainers()

  /** Perform cleanup on exit - to be registered as shutdown hook */
  override def cleanup(): Unit = {
    implicit val transid = TransactionId.invoker
    try {
      removeAllActionContainers()
    } catch {
      case e: Exception => logging.error(this, s"Failed to remove action containers: ${e.getMessage}")
    }
  }

  /**
   * Removes all wsk_ containers - regardless of their state
   *
   * If the system in general or Docker in particular has a very
   * high load, commands may take longer than the specified time
   * resulting in an exception.
   *
   * There is no checking whether container removal was successful
   * or not.
   *
   * @throws InterruptedException     if the current thread is interrupted while waiting
   * @throws TimeoutException         if after waiting for the specified time this `Awaitable` is still not ready
   */
  @throws(classOf[TimeoutException])
  @throws(classOf[InterruptedException])
  private def removeAllActionContainers(): Unit = {
    implicit val transid = TransactionId.invoker
    val cleaning =
      docker.ps(filters = Seq("name" -> s"${ContainerFactory.containerNamePrefix(instance)}_"), all = true).flatMap {
        containers =>
          logging.info(this, s"removing ${containers.size} action containers.")
          val removals = containers.map { id =>
            (if (dockerContainerFactoryConfig.useRunc) {
               runc.resume(id)
             } else {
               docker.unpause(id)
             })
              .recoverWith {
                // Ignore resume failures and try to remove anyway
                case _ => Future.successful(())
              }
              .flatMap { _ =>
                docker.rm(id)
              }
          }
          Future.sequence(removals)
      }
    Await.ready(cleaning, 30.seconds)
  }
}

object DockerContainerFactoryProvider extends ContainerFactoryProvider {
  override def instance(actorSystem: ActorSystem,
                        logging: Logging,
                        config: WhiskConfig,
                        instanceId: InvokerInstanceId,
                        parameters: Map[String, Set[String]]): ContainerFactory = {

    new DockerContainerFactory(instanceId, parameters)(
      actorSystem,
      actorSystem.dispatcher,
      logging,
      new DockerClientWithFileAccess()(actorSystem.dispatcher)(logging, actorSystem),
      new RuncClient()(actorSystem.dispatcher)(logging, actorSystem))
  }

}