content: /*
   * 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.camel.karavan.docker;
  
  import com.github.dockerjava.api.DockerClient;
  import com.github.dockerjava.api.command.*;
  import com.github.dockerjava.api.model.*;
  import com.github.dockerjava.core.DefaultDockerClientConfig;
  import com.github.dockerjava.core.DockerClientConfig;
  import com.github.dockerjava.core.DockerClientImpl;
  import com.github.dockerjava.core.command.ExecStartResultCallback;
  import com.github.dockerjava.transport.DockerHttpClient;
  import com.github.dockerjava.zerodep.ZerodepDockerHttpClient;
  import io.quarkus.runtime.ShutdownEvent;
  import io.quarkus.runtime.StartupEvent;
  import io.vertx.core.Vertx;
  import io.vertx.core.buffer.Buffer;
  import jakarta.enterprise.context.ApplicationScoped;
  import jakarta.enterprise.event.Observes;
  import jakarta.inject.Inject;
  import org.apache.camel.karavan.model.ContainerImage;
  import org.apache.camel.karavan.model.ContainerType;
  import org.apache.camel.karavan.model.DockerComposeService;
  import org.apache.camel.karavan.model.DockerComposeVolume;
  import org.apache.camel.karavan.service.CodeService;
  import org.apache.camel.karavan.service.ConfigService;
  import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
  import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
  import org.apache.commons.io.IOUtils;
  import org.apache.commons.lang3.math.NumberUtils;
  import org.eclipse.microprofile.config.inject.ConfigProperty;
  import org.jboss.logging.Logger;
  
  import java.io.ByteArrayInputStream;
  import java.io.ByteArrayOutputStream;
  import java.io.File;
  import java.io.IOException;
  import java.nio.file.Files;
  import java.nio.file.Paths;
  import java.util.*;
  import java.util.stream.Collectors;
  
  import static org.apache.camel.karavan.KaravanConstants.LABEL_PROJECT_ID;
  import static org.apache.camel.karavan.KaravanConstants.LABEL_TYPE;
  
  @ApplicationScoped
  public class DockerService {
  
      public enum PULL_IMAGE {
          always, ifNotExists, never
      }
  
      private static final Logger LOGGER = Logger.getLogger(DockerService.class.getName());
  
      @ConfigProperty(name = "karavan.docker.network")
      String networkName;
  
      @ConfigProperty(name = "karavan.container-image.registry")
      String registry;
      @ConfigProperty(name = "karavan.container-image.group")
      String group;
      @ConfigProperty(name = "karavan.container-image.registry-username")
      Optional<String> username;
      @ConfigProperty(name = "karavan.container-image.registry-password")
      Optional<String> password;
  
      @Inject
      DockerEventHandler dockerEventHandler;
  
      @Inject
      CodeService codeService;
  
      @Inject
      Vertx vertx;
  
      private DockerClient dockerClient;
  
      private DockerClient dockerClientConnectedToRegistry;
  
      void onStart(@Observes StartupEvent ev) {
          if (!ConfigService.inKubernetes()) {
              try (EventsCmd cmd = getDockerClient().eventsCmd()) {
                  cmd.exec(dockerEventHandler);
              }
          }
      }
  
      void onStop(@Observes ShutdownEvent ev) throws IOException {
          if (!ConfigService.inKubernetes()) {
              dockerEventHandler.close();
          }
      }
  
      public boolean checkDocker() {
          try {
              try (PingCmd cmd = getDockerClient().pingCmd()) {
                  cmd.exec();
              }
              LOGGER.info("Docker is available");
              return true;
          } catch (Exception e) {
              LOGGER.error("Error connecting Docker: " + e.getMessage());
              return false;
          }
      }
  
      public Info getInfo() {
          try (InfoCmd cmd = getDockerClient().infoCmd()) {
              return cmd.exec();
          }
      }
  
      public Container getContainer(String id) {
          try (ListContainersCmd cmd = getDockerClient().listContainersCmd().withShowAll(true).withIdFilter(List.of(id))) {
              List<Container> containers = cmd.exec();
              return containers.isEmpty() ? null : containers.get(0);
          }
      }
  
      public Container getContainerByName(String name) {
          List<Container> containers = findContainer(name);
          return !containers.isEmpty() ? containers.get(0) : null;
      }
  
      public List<Container> getAllContainers() {
          try (ListContainersCmd cmd = getDockerClient().listContainersCmd().withShowAll(true)) {
              return cmd.exec();
          }
      }
  
      public Container createContainerFromCompose(DockerComposeService compose, Map<String, String> labels, PULL_IMAGE pullImage) throws InterruptedException {
          List<Container> containers = findContainer(compose.getContainer_name());
          if (containers.isEmpty()) {
              HealthCheck healthCheck = DockerUtils.getHealthCheck(compose.getHealthcheck());
  
              List<String> env = compose.getEnvironmentList();
  
              LOGGER.infof("Compose Service started for %s in network:%s", compose.getContainer_name(), networkName);
  
              RestartPolicy restartPolicy = RestartPolicy.noRestart();
              if (Objects.equals(compose.getRestart(), RestartPolicy.onFailureRestart(10).getName())) {
                  restartPolicy = RestartPolicy.onFailureRestart(10);
              } else if (Objects.equals(compose.getRestart(), RestartPolicy.alwaysRestart().getName())) {
                  restartPolicy = RestartPolicy.alwaysRestart();
              }
  
              return createContainer(compose.getContainer_name(), compose.getImage(),
                      env, compose.getPortsMap(), healthCheck, labels, compose.getVolumes(), networkName, restartPolicy, pullImage,
                      compose.getCpus(), compose.getCpu_percent(), compose.getMem_limit(), compose.getMem_reservation(), compose.getCommand());
  
          } else {
              LOGGER.info("Compose Service already exists: " + containers.get(0).getId());
              return containers.get(0);
          }
      }
  
      public List<Container> findContainer(String containerName) {
          try (ListContainersCmd cmd = getDockerClient().listContainersCmd().withShowAll(true).withNameFilter(List.of(containerName))) {
              return cmd.exec().stream().filter(c -> Objects.equals(c.getNames()[0].replaceFirst("/", ""), containerName)).toList();
          }
      }
  
      public Container createContainer(String name, String image, List<String> env, Map<Integer, Integer> ports,
                                       HealthCheck healthCheck, Map<String, String> labels,
                                       List<DockerComposeVolume> volumes, String network, RestartPolicy restartPolicy,
                                       PULL_IMAGE pullImage, String cpus, String cpu_percent, String mem_limit, String mem_reservation,
                                       String dockerCommand) throws InterruptedException {
          List<Container> containers = findContainer(name);
          if (containers.isEmpty()) {
              if (Objects.equals(labels.get(LABEL_TYPE), ContainerType.devmode.name())
                      || Objects.equals(labels.get(LABEL_TYPE), ContainerType.build.name())
                      || Objects.equals(labels.get(LABEL_TYPE), ContainerType.devservice.name())) {
                  LOGGER.info("Pulling DevMode image from DockerHub: " + image);
                  pullImageFromDockerHub(image, Objects.equals(pullImage, PULL_IMAGE.always));
              }
              if (Objects.equals(labels.get(LABEL_TYPE), ContainerType.packaged.name())) {
                  LOGGER.info("Pulling Project image from Registry: " + image);
                  pullImage(image, Objects.equals(pullImage, PULL_IMAGE.always));
              }
  
              Ports portBindings = DockerUtils.getPortBindings(ports);
              List<ExposedPort> exposePorts = DockerUtils.getExposedPorts(ports);
              try (CreateContainerCmd createContainerCmd = getDockerClient().createContainerCmd(image)
                      .withName(name).withLabels(labels).withEnv(env).withHostName(name).withExposedPorts(exposePorts).withHealthcheck(healthCheck)) {
  
                  List<Mount> mounts = new ArrayList<>();
                  if (volumes != null && !volumes.isEmpty()) {
                      volumes.forEach(volume -> {
                          var mount = new Mount().withType(MountType.valueOf(volume.getType().toUpperCase())).withTarget(volume.getTarget());
                          if (volume.getSource() != null) {
                              mount = mount.withSource(volume.getSource());
                          }
                          mounts.add(mount);
                      });
                  }
                  if (dockerCommand != null) {
                      createContainerCmd.withCmd("/bin/sh", "-c", dockerCommand);
                      System.out.println(dockerCommand);
                  }
                  if (Objects.equals(labels.get(LABEL_PROJECT_ID), ContainerType.build.name())) {
                      mounts.add(new Mount().withType(MountType.BIND).withSource("/var/run/docker.sock").withTarget("/var/run/docker.sock"));
                  }
  
                  createContainerCmd.withHostConfig(new HostConfig()
                          .withRestartPolicy(restartPolicy)
                          .withPortBindings(portBindings)
                          .withMounts(mounts)
                          .withMemory(DockerUtils.parseMemory(mem_limit))
                          .withMemoryReservation(DockerUtils.parseMemory(mem_reservation))
                          .withCpuPercent(NumberUtils.toLong(cpu_percent))
                          .withNanoCPUs(NumberUtils.toLong(cpus))
                          .withNetworkMode(network != null ? network : networkName));
  
                  CreateContainerResponse response = createContainerCmd.exec();
                  LOGGER.info("Container created: " + response.getId());
  
                  try (ListContainersCmd cmd = getDockerClient().listContainersCmd().withShowAll(true).withIdFilter(Collections.singleton(response.getId()))) {
                      return cmd.exec().get(0);
                  }
              }
          } else {
              LOGGER.info("Container already exists: " + containers.get(0).getId());
              return containers.get(0);
          }
      }
  
      public void runContainer(String name) {
          List<Container> containers = findContainer(name);
          if (containers.size() == 1) {
              runContainer(containers.get(0));
          }
      }
  
      public void runContainer(Container container) {
          if (container.getState().equals("paused")) {
              try (UnpauseContainerCmd cmd = getDockerClient().unpauseContainerCmd(container.getId())) {
                  cmd.exec();
              }
          } else if (!container.getState().equals("running")) {
              try (StartContainerCmd cmd = getDockerClient().startContainerCmd(container.getId())) {
                  cmd.exec();
              }
          }
      }
  
      protected void copyFiles(String containerId, String containerPath, Map<String, String> files, boolean dirChildrenOnly) throws IOException {
          String temp = codeService.saveProjectFilesInTemp(files);
          dockerClient.copyArchiveToContainerCmd(containerId).withRemotePath(containerPath)
                  .withDirChildrenOnly(dirChildrenOnly).withHostResource(temp).exec();
      }
  
      public void copyExecFile(String containerId, String containerPath, String filename, String script) {
          String temp = vertx.fileSystem().createTempDirectoryBlocking(containerId);
          String path = temp + File.separator + filename;
          vertx.fileSystem().writeFileBlocking(path, Buffer.buffer(script));
  
          try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
               TarArchiveOutputStream tarArchive = new TarArchiveOutputStream(byteArrayOutputStream)) {
              tarArchive.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX);
              tarArchive.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_POSIX);
  
              TarArchiveEntry tarEntry = new TarArchiveEntry(new File(path));
              tarEntry.setName(filename);
              tarEntry.setMode(0755);
              tarArchive.putArchiveEntry(tarEntry);
              IOUtils.write(Files.readAllBytes(Paths.get(path)), tarArchive);
              tarArchive.closeArchiveEntry();
              tarArchive.finish();
  
              dockerClient.copyArchiveToContainerCmd(containerId)
                      .withTarInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()))
                      .withRemotePath(containerPath).exec();
          } catch (Exception e) {
              LOGGER.error(e.getMessage());
              e.printStackTrace();
          }
      }
  
      public void logContainer(String containerName, DockerLogCallback callback) {
          try {
              Container container = getContainerByName(containerName);
              if (container != null) {
                  try (LogContainerCmd cmd = getDockerClient().logContainerCmd(container.getId())
                          .withStdOut(true)
                          .withStdErr(true)
                          .withTimestamps(false)
                          .withFollowStream(true)
                          .withTail(100)) {
                      cmd.exec(callback);
                      callback.awaitCompletion();
                  }
              }
          } catch (Exception e) {
              LOGGER.error(e.getMessage());
          }
      }
  
      public void pauseContainer(String name) {
          List<Container> containers = findContainer(name);
          if (containers.size() == 1) {
              Container container = containers.get(0);
              if (container.getState().equals("running")) {
                  try (PauseContainerCmd cmd = getDockerClient().pauseContainerCmd(container.getId())) {
                      cmd.exec();
                  }
              }
          }
      }
  
      public void stopContainer(String name) {
          List<Container> containers = findContainer(name);
          if (containers.size() == 1) {
              Container container = containers.get(0);
              if (container.getState().equals("running") || container.getState().equals("paused")) {
                  try (StopContainerCmd cmd = getDockerClient().stopContainerCmd(container.getId()).withTimeout(1)) {
                      cmd.exec();
                  }
              }
          }
      }
  
      public void deleteContainer(String name) {
          List<Container> containers = findContainer(name);
          if (containers.size() == 1) {
              Container container = containers.get(0);
              try (RemoveContainerCmd cmd = getDockerClient().removeContainerCmd(container.getId()).withForce(true)) {
                  cmd.exec();
              }
          }
      }
  
      public void execCommandInContainer(String containerName, String cmd) throws InterruptedException {
          List<Container> containers = findContainer(containerName);
          if (containers.size() == 1) {
              Container container = containers.get(0);
              if (container.getState().equals("running")) {
                  try (ExecCreateCmd execCreateCmd = getDockerClient().execCreateCmd(container.getId()).withAttachStdout(true).withAttachStderr(true).withCmd(cmd.split("\\s+"))) {
                      var execCreateCmdResponse = execCreateCmd.exec();
                      try (ExecStartCmd execStartCmd = getDockerClient().execStartCmd(execCreateCmdResponse.getId())) {
                          execStartCmd.exec(new ExecStartResultCallback(System.out, System.err)).awaitCompletion();
                      }
  
  
                  }
              }
          }
      }
  
      public void pullImage(String image, boolean pullAlways) throws InterruptedException {
          try (ListImagesCmd cmd = getDockerClient().listImagesCmd().withShowAll(true)) {
              List<Image> images = cmd.exec();
              List<String> tags = images.stream()
                      .map(i -> Arrays.stream(i.getRepoTags()).collect(Collectors.toList()))
                      .flatMap(Collection::stream)
                      .toList();
  
              if (pullAlways || images.stream().noneMatch(i -> tags.contains(image))) {
                  var callback = new DockerPullCallback(LOGGER::info);
                  try (PullImageCmd pullImageCmd = getDockerClient().pullImageCmd(image)) {
                      pullImageCmd.exec(callback);
                      callback.awaitCompletion();
                  }
              }
          }
      }
  
      public void pullImageFromDockerHub(String image, boolean pullAlways) throws InterruptedException {
          try (ListImagesCmd cmd = getDockerClientNotConnectedToRegistry().listImagesCmd().withShowAll(true)) {
              List<Image> images = cmd.exec();
              List<String> tags = images.stream()
                      .filter(i -> i.getRepoTags() != null)
                      .map(i -> Arrays.stream(i.getRepoTags()).collect(Collectors.toList()))
                      .flatMap(Collection::stream)
                      .toList();
  
              if (pullAlways || images.stream().noneMatch(i -> tags.contains(image))) {
                  var callback = new DockerPullCallback(LOGGER::info);
                  try (PullImageCmd pullImageCmd = getDockerClientNotConnectedToRegistry().pullImageCmd(image)) {
                      pullImageCmd.exec(callback);
                      callback.awaitCompletion();
                  }
              }
          }
      }
  
      public void pullImagesForProject(String projectId) throws InterruptedException {
          if (!Objects.equals(registry, "registry:5000") && username.isPresent() && password.isPresent()) {
              var repository = registry + "/" + group + "/" + projectId;
              try (PullImageCmd cmd = getDockerClient().pullImageCmd(repository)) {
                  var callback = new DockerPullCallback(LOGGER::info);
                  cmd.exec(callback);
                  callback.awaitCompletion();
              }
          }
      }
  
      private DockerClientConfig getDockerClientConfig(boolean connectedToRegistry) {
          LOGGER.info("Docker Client Configuring " + (connectedToRegistry ? "( connectedToRegistry)" : ""));
          DefaultDockerClientConfig.Builder builder = DefaultDockerClientConfig.createDefaultConfigBuilder();
          if (connectedToRegistry) {
              LOGGER.info("Docker Client Registry " + registry);
              LOGGER.info("Docker Client Username " + (username.isPresent() ? "is not empty " : "is empty"));
              LOGGER.info("Docker Client Password " + (password.isPresent() ? "is not empty " : "is empty"));
              if (!Objects.equals(registry, "registry:5000") && username.isPresent() && password.isPresent()) {
                  builder.withRegistryUrl(registry);
                  builder.withRegistryUsername(username.get());
                  builder.withRegistryPassword(password.get());
              }
          }
          return builder.build();
      }
  
      private DockerHttpClient getDockerHttpClient(DockerClientConfig config) {
          return new ZerodepDockerHttpClient.Builder()
                  .dockerHost(config.getDockerHost())
                  .sslConfig(config.getSSLConfig())
                  .maxConnections(100)
                  .build();
      }
  
      public DockerClient getDockerClient() {
          if (dockerClient == null) {
              DockerClientConfig config = getDockerClientConfig(true);
              DockerHttpClient httpClient = getDockerHttpClient(config);
              dockerClient = DockerClientImpl.getInstance(config, httpClient);
          }
          return dockerClient;
      }
  
      public DockerClient getDockerClientNotConnectedToRegistry() {
          if (dockerClientConnectedToRegistry == null) {
              DockerClientConfig config = getDockerClientConfig(false);
              DockerHttpClient httpClient = getDockerHttpClient(config);
              dockerClientConnectedToRegistry = DockerClientImpl.getInstance(config, httpClient);
          }
          return dockerClientConnectedToRegistry;
      }
  
      public int getMaxPortMapped(int port) {
          try (ListContainersCmd cmd = getDockerClient().listContainersCmd().withShowAll(true)) {
              return cmd.exec().stream()
                      .map(c -> List.of(c.ports))
                      .flatMap(List::stream)
                      .filter(p -> Objects.equals(p.getPrivatePort(), port))
                      .map(ContainerPort::getPublicPort).filter(Objects::nonNull)
                      .mapToInt(Integer::intValue)
                      .max().orElse(port);
          }
      }
  
      public List<ContainerImage> getImages() {
          try (ListImagesCmd cmd = getDockerClient().listImagesCmd().withShowAll(true)) {
              return cmd.exec().stream()
                      .filter(image -> image != null && image.getRepoTags() != null && image.getRepoTags().length > 0)
                      .map(image -> new ContainerImage(image.getId(), image.getRepoTags()[0], image.getCreated(), image.getSize()))
                      .toList();
          }
      }
  
      public void deleteImage(String imageName) {
          try (ListImagesCmd listImagesCmd = getDockerClient().listImagesCmd().withShowAll(true)) {
              Optional<Image> image = listImagesCmd.exec().stream()
                      .filter(i -> Arrays.asList(i.getRepoTags()).contains(imageName)).findFirst();
              if (image.isPresent()) {
                  try (RemoveImageCmd removeImageCmd = getDockerClient().removeImageCmd(image.get().getId())) {
                      removeImageCmd.exec();
                  }
              }
          }
      }
  }
  