package ai.decompile.docker.service;

import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.async.ResultCallback;
import com.github.dockerjava.api.command.BuildImageResultCallback;
import com.github.dockerjava.api.command.WaitContainerResultCallback;
import com.github.dockerjava.api.exception.NotFoundException;
import com.github.dockerjava.api.model.Bind;
import com.github.dockerjava.api.model.BuildResponseItem;
import com.github.dockerjava.api.model.Frame;
import com.github.dockerjava.api.model.HostConfig;
import java.io.Closeable;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;

@Log4j2
@Service
@RequiredArgsConstructor
@ConditionalOnProperty(name = "app.docker.enabled", havingValue = "true")
public class ContainerService {
  private final DockerClient dockerClient;

  public void ensureImage(String imageTag, File dockerfile) {
    try {
      dockerClient.inspectImageCmd(imageTag).exec();
      log.debug("Image '{}' found locally", imageTag);
    } catch (NotFoundException e) {
      log.info("Image '{}' not found, building from {}", imageTag, dockerfile.getName());
      buildImage(imageTag, dockerfile);
    }
  }

  public String createContainer(String image, String[] cmd, Bind... binds) {
    return dockerClient
        .createContainerCmd(image)
        .withCmd(cmd)
        .withHostConfig(HostConfig.newHostConfig().withBinds(binds))
        .exec()
        .getId();
  }

  public void startContainer(String containerId) {
    dockerClient.startContainerCmd(containerId).exec();
  }

  public int waitForExit(String containerId, long timeoutSeconds) {
    return dockerClient
        .waitContainerCmd(containerId)
        .exec(new WaitContainerResultCallback())
        .awaitStatusCode(timeoutSeconds, TimeUnit.SECONDS);
  }

  public String captureLogs(String containerId, long timeoutSeconds) {
    var collector = new FrameCollector();
    try (collector) {
      dockerClient
          .logContainerCmd(containerId)
          .withStdOut(true)
          .withStdErr(true)
          .withFollowStream(true)
          .exec(collector);

      collector.awaitCompletion(timeoutSeconds, TimeUnit.SECONDS);
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
    } catch (Exception e) {
      log.warn("Failed to capture container logs for {}: {}", containerId, e.getMessage());
    }
    return collector.toString();
  }

  public void stopAndRemove(String containerId) {
    try {
      dockerClient.stopContainerCmd(containerId).withTimeout(10).exec();
    } catch (Exception e) {
      log.debug(
          "Failed to stop container {} (may already be stopped): {}", containerId, e.getMessage());
    }
    try {
      dockerClient.removeContainerCmd(containerId).withForce(true).exec();
    } catch (Exception e) {
      log.debug(
          "Failed to remove container {} (may already be removed): {}",
          containerId,
          e.getMessage());
    }
  }

  private void buildImage(String imageTag, File dockerfile) {
    var absoluteDockerfile = dockerfile.getAbsoluteFile();
    if (!absoluteDockerfile.exists()) {
      throw new RuntimeException("Dockerfile not found at " + absoluteDockerfile.getAbsolutePath());
    }

    Set<String> tags = new HashSet<>();
    tags.add(imageTag);

    var callback =
        dockerClient
            .buildImageCmd(absoluteDockerfile)
            .withTags(tags)
            .exec(
                new BuildImageResultCallback() {
                  @Override
                  public void onNext(BuildResponseItem item) {
                    if (Objects.nonNull(item.getStream())) {
                      log.debug("Build: {}", item.getStream().trim());
                    }

                    if (item.isErrorIndicated() && Objects.nonNull(item.getErrorDetail())) {
                      log.error("Build error: {}", item.getErrorDetail().getMessage());
                    }

                    super.onNext(item);
                  }
                });

    try {
      callback.awaitCompletion();
      log.info("Image '{}' built successfully", imageTag);
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
      throw new RuntimeException("Interrupted while building image " + imageTag, e);
    } catch (Exception e) {
      throw new RuntimeException("Failed to build image " + imageTag + ": " + e.getMessage(), e);
    }
  }

  private static class FrameCollector extends ResultCallback.Adapter<Frame> implements Closeable {
    private final StringBuilder buffer = new StringBuilder();

    @Override
    public void onNext(Frame frame) {
      buffer.append(new String(frame.getPayload(), StandardCharsets.UTF_8));
    }

    @Override
    public void close() {
      // no-op
    }

    @Override
    public String toString() {
      return buffer.toString();
    }
  }
}
