package com.cameleer.server.app.runtime;

import com.cameleer.server.core.runtime.ContainerRequest;
import com.cameleer.server.core.runtime.ContainerStatus;
import com.cameleer.server.core.runtime.RuntimeOrchestrator;
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.async.ResultCallback;
import com.github.dockerjava.api.model.AccessMode;
import com.github.dockerjava.api.model.Bind;
import com.github.dockerjava.api.model.Capability;
import com.github.dockerjava.api.model.Frame;
import com.github.dockerjava.api.model.HealthCheck;
import com.github.dockerjava.api.model.HostConfig;
import com.github.dockerjava.api.model.RestartPolicy;
import com.github.dockerjava.api.model.Volume;
import jakarta.annotation.PreDestroy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

public class DockerRuntimeOrchestrator implements RuntimeOrchestrator {

    private static final Logger log = LoggerFactory.getLogger(DockerRuntimeOrchestrator.class);

    /** Sandboxed runtime we prefer when the daemon has it registered. */
    private static final String SANDBOX_RUNTIME = "runsc";

    /** Hard cap on processes/threads per tenant container. Spring Boot + Camel
     * + a Kafka client comfortably fits in 512; raise via daemon-wide limits if
     * a tenant legitimately needs more (and revisit the multi-tenancy threat
     * model when that happens). */
    private static final long PIDS_LIMIT = 512L;

    /** /tmp must be writeable for JVM tmpdir, JIT scratch, and JNI native lib
     * unpacking (Netty tcnative, Snappy, LZ4, Zstd all dlopen from here).
     * `noexec` would block dlopen via mmap(PROT_EXEC) — keep it off. */
    private static final String TMPFS_TMP_OPTS = "rw,nosuid,size=256m";

    private final DockerClient dockerClient;
    private final String dockerRuntime;

    private ContainerLogForwarder logForwarder;

    public DockerRuntimeOrchestrator(DockerClient dockerClient) {
        this(dockerClient, "");
    }

    public DockerRuntimeOrchestrator(DockerClient dockerClient, String runtimeOverride) {
        this.dockerClient = dockerClient;
        this.dockerRuntime = resolveRuntime(runtimeOverride);
    }

    private String resolveRuntime(String override) {
        if (override != null && !override.isBlank()) {
            log.info("Container runtime forced to '{}' via cameleer.server.runtime.dockerruntime", override);
            return override;
        }
        try {
            Map<String, ?> runtimes = dockerClient.infoCmd().exec().getRuntimes();
            if (runtimes != null && runtimes.containsKey(SANDBOX_RUNTIME)) {
                log.info("gVisor ({}) detected — sandboxed runtime will be used for tenant containers",
                        SANDBOX_RUNTIME);
                return SANDBOX_RUNTIME;
            }
        } catch (Exception e) {
            log.warn("Could not query Docker runtimes: {} — falling back to daemon default", e.getMessage());
        }
        log.info("No sandboxed runtime detected — using Docker default (runc). Install gVisor on the host "
                + "for tenant kernel isolation; see issue #152.");
        return "";
    }

    /** Visible for tests / introspection. Empty string = let Docker pick its default. */
    String getDockerRuntime() {
        return dockerRuntime;
    }

    public void setLogForwarder(ContainerLogForwarder logForwarder) {
        this.logForwarder = logForwarder;
    }

    @PreDestroy
    public void close() throws IOException {
        if (dockerClient != null) {
            dockerClient.close();
        }
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    @Override
    public void pullImage(String image) {
        try {
            log.info("Pulling image {}", image);
            dockerClient.pullImageCmd(image).start().awaitCompletion();
            log.info("Image pulled: {}", image);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            log.warn("Image pull interrupted for {}", image);
        } catch (Exception e) {
            log.warn("Failed to pull image {} (will use local cache if available): {}", image, e.getMessage());
        }
    }

    @Override
    public String startContainer(ContainerRequest request) {
        List<String> envList = request.envVars().entrySet().stream()
                .map(e -> e.getKey() + "=" + e.getValue()).toList();

        // Tenant containers run untrusted user JVMs — every tenant JAR can call
        // Runtime.exec, reflective bean dispatch, MVEL/Groovy templating. Java 17
        // has no SecurityManager, so isolation MUST live below the JVM.
        // See issue #152 for the full threat model. Defaults are fail-closed:
        // - cap_drop ALL: outbound TCP still works (no caps needed); raw sockets,
        //   ptrace, mounts, and bind <1024 are all denied.
        // - no-new-privileges: setuid binaries cannot escalate.
        // - apparmor=docker-default: Docker's stock MAC profile.
        //   Daemon's default seccomp profile is applied implicitly when no
        //   `seccomp=` override is set — no need to declare it.
        // - readonly rootfs + /tmp tmpfs: persistence-via-write defeated; apps
        //   needing durable state declare writeableVolumes (issue #153).
        // - pids-limit: fork bombs cannot exhaust the host PID namespace.
        HostConfig hostConfig = HostConfig.newHostConfig()
                .withMemory(request.memoryLimitBytes())
                .withMemorySwap(request.memoryLimitBytes())
                .withCpuShares(request.cpuShares())
                .withNetworkMode(request.network())
                .withRestartPolicy(RestartPolicy.onFailureRestart(request.restartPolicyMaxRetries()))
                .withCapDrop(Capability.values())
                .withSecurityOpts(List.of(
                        "no-new-privileges:true",
                        "apparmor=docker-default"))
                .withReadonlyRootfs(true)
                .withPidsLimit(PIDS_LIMIT)
                .withTmpFs(Map.of("/tmp", TMPFS_TMP_OPTS));

        if (!dockerRuntime.isBlank()) {
            hostConfig.withRuntime(dockerRuntime);
        }

        // JAR mounting: volume mount (Docker-in-Docker) or bind mount (host path)
        if (request.jarVolumeName() != null && !request.jarVolumeName().isBlank()) {
            // Mount the named volume at the jar storage base path
            Bind volumeBind = new Bind(request.jarVolumeName(), new Volume(request.jarVolumeMountPath()), AccessMode.ro);
            hostConfig.withBinds(volumeBind);
        } else {
            Bind jarBind = new Bind(request.jarPath(), new Volume("/app/app.jar"), AccessMode.ro);
            hostConfig.withBinds(jarBind);
        }

        if (request.memoryReserveBytes() != null) {
            hostConfig.withMemoryReservation(request.memoryReserveBytes());
        }
        if (request.cpuQuota() != null) {
            hostConfig.withCpuQuota(request.cpuQuota());
        }

        // Resolve the JAR path for the entrypoint
        String appJarPath;
        if (request.jarVolumeName() != null && !request.jarVolumeName().isBlank()) {
            appJarPath = request.jarPath();
        } else {
            appJarPath = "/app/app.jar";
        }

        var createCmd = dockerClient.createContainerCmd(request.baseImage())
                .withName(request.containerName())
                .withEnv(envList)
                .withLabels(request.labels() != null ? request.labels() : Map.of())
                .withHostConfig(hostConfig)
                .withHealthcheck(new HealthCheck()
                        .withTest(List.of("CMD-SHELL",
                                "wget -qO- http://localhost:" + request.healthCheckPort() + "/cameleer/health || exit 1"))
                        .withInterval(10_000_000_000L)
                        .withTimeout(5_000_000_000L)
                        .withRetries(3)
                        .withStartPeriod(30_000_000_000L));

        // Build entrypoint based on runtime type
        String customArgs = request.customArgs() != null && !request.customArgs().isBlank()
                ? " " + request.customArgs() : "";
        String entrypoint = switch (request.runtimeType()) {
            case "plain-java" -> "exec java -javaagent:/app/agent.jar" + customArgs +
                    " -cp " + appJarPath + " " + request.mainClass();
            case "native" -> "exec " + appJarPath + customArgs;
            default -> // spring-boot, quarkus, and others all use -jar
                    "exec java -javaagent:/app/agent.jar" + customArgs + " -jar " + appJarPath;
        };
        createCmd.withEntrypoint("sh", "-c", entrypoint);

        if (request.exposedPorts() != null && !request.exposedPorts().isEmpty()) {
            var ports = request.exposedPorts().stream()
                    .map(p -> com.github.dockerjava.api.model.ExposedPort.tcp(p))
                    .toArray(com.github.dockerjava.api.model.ExposedPort[]::new);
            createCmd.withExposedPorts(ports);
        }

        var container = createCmd.exec();
        dockerClient.startContainerCmd(container.getId()).exec();

        log.info("Started container {} ({})", request.containerName(), container.getId());
        return container.getId();
    }

    public DockerClient getDockerClient() {
        return dockerClient;
    }

    @Override
    public void stopContainer(String containerId) {
        try {
            dockerClient.stopContainerCmd(containerId).withTimeout(30).exec();
            log.info("Stopped container {}", containerId);
        } catch (Exception e) {
            log.warn("Failed to stop container {}: {}", containerId, e.getMessage());
        }
    }

    @Override
    public void removeContainer(String containerId) {
        try {
            dockerClient.removeContainerCmd(containerId).withForce(true).exec();
            log.info("Removed container {}", containerId);
        } catch (Exception e) {
            log.warn("Failed to remove container {}: {}", containerId, e.getMessage());
        }
    }

    @Override
    public ContainerStatus getContainerStatus(String containerId) {
        try {
            var inspection = dockerClient.inspectContainerCmd(containerId).exec();
            var state = inspection.getState();
            var health = state.getHealth();
            var healthStatus = health != null ? health.getStatus() : null;
            // Use health status if available, otherwise fall back to container state
            var effectiveState = healthStatus != null ? healthStatus : state.getStatus();
            return new ContainerStatus(
                    effectiveState,
                    Boolean.TRUE.equals(state.getRunning()),
                    state.getExitCodeLong() != null ? state.getExitCodeLong().intValue() : 0,
                    state.getError());
        } catch (Exception e) {
            return new ContainerStatus("not_found", false, -1, e.getMessage());
        }
    }

    @Override
    public Stream<String> getLogs(String containerId, int tailLines) {
        List<String> logLines = new ArrayList<>();
        try {
            dockerClient.logContainerCmd(containerId)
                    .withStdOut(true)
                    .withStdErr(true)
                    .withTail(tailLines)
                    .withTimestamps(true)
                    .exec(new ResultCallback.Adapter<Frame>() {
                        @Override
                        public void onNext(Frame frame) {
                            logLines.add(new String(frame.getPayload()).trim());
                        }
                    }).awaitCompletion();
        } catch (Exception e) {
            log.warn("Failed to get logs for container {}: {}", containerId, e.getMessage());
        }
        return logLines.stream();
    }

    @Override
    public void startLogCapture(String containerId, String instanceId, String appSlug, String envSlug, String tenantId) {
        if (logForwarder != null) {
            logForwarder.startCapture(containerId, instanceId, appSlug, envSlug, tenantId);
        }
    }

    @Override
    public void stopLogCapture(String containerId) {
        if (logForwarder != null) {
            logForwarder.stopCapture(containerId);
        }
    }
}
