package net.siegeln.cameleer.saas.provisioning;

import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.command.CreateContainerResponse;
import com.github.dockerjava.api.command.InspectContainerResponse;
import com.github.dockerjava.api.exception.NotFoundException;
import com.github.dockerjava.api.model.*;
import com.github.dockerjava.core.DockerClientConfig;
import com.github.dockerjava.core.DockerClientImpl;
import com.github.dockerjava.transport.DockerHttpClient;
import com.github.dockerjava.zerodep.ZerodepDockerHttpClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.Duration;
import java.util.List;
import java.util.Map;

public class DockerTenantProvisioner implements TenantProvisioner {
    private static final Logger log = LoggerFactory.getLogger(DockerTenantProvisioner.class);

    private final DockerClient docker;
    private final ProvisioningProperties props;

    public DockerTenantProvisioner(DockerClientConfig config, ProvisioningProperties props) {
        this.props = props;
        DockerHttpClient httpClient = new ZerodepDockerHttpClient.Builder()
            .dockerHost(config.getDockerHost())
            .maxConnections(10)
            .connectionTimeout(Duration.ofSeconds(5))
            .responseTimeout(Duration.ofSeconds(30))
            .build();
        this.docker = DockerClientImpl.getInstance(config, httpClient);
    }

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

    @Override
    public ProvisionResult provision(TenantProvisionRequest req) {
        String serverName = serverContainerName(req.slug());
        String uiName = uiContainerName(req.slug());
        String tenantNetwork = tenantNetworkName(req.slug());
        String endpoint = "http://" + serverName + ":8081";

        try {
            pullIfMissing(props.serverImage());
            pullIfMissing(props.serverUiImage());

            // Create isolated tenant network
            ensureNetwork(tenantNetwork);

            createServerContainer(req, serverName, tenantNetwork);
            docker.startContainerCmd(serverName).exec();

            createUiContainer(req.slug(), uiName, serverName, tenantNetwork);
            docker.startContainerCmd(uiName).exec();

            if (!waitForHealth(serverName, 60)) {
                return ProvisionResult.fail("Server did not become healthy within 60s");
            }

            log.info("Provisioned tenant '{}': server={}, ui={}", req.slug(), serverName, uiName);
            return ProvisionResult.ok(endpoint);
        } catch (Exception e) {
            log.error("Failed to provision tenant '{}'", req.slug(), e);
            return ProvisionResult.fail(e.getMessage());
        }
    }

    @Override
    public void start(String slug) {
        try {
            docker.startContainerCmd(serverContainerName(slug)).exec();
            docker.startContainerCmd(uiContainerName(slug)).exec();
        } catch (Exception e) {
            log.error("Failed to start containers for '{}'", slug, e);
            throw new RuntimeException("Start failed: " + e.getMessage(), e);
        }
    }

    @Override
    public void stop(String slug) {
        try {
            stopIfRunning(serverContainerName(slug));
            stopIfRunning(uiContainerName(slug));
        } catch (Exception e) {
            log.error("Failed to stop containers for '{}'", slug, e);
            throw new RuntimeException("Stop failed: " + e.getMessage(), e);
        }
    }

    @Override
    public void remove(String slug) {
        removeContainer(uiContainerName(slug));
        removeContainer(serverContainerName(slug));
        removeNetwork(tenantNetworkName(slug));
    }

    @Override
    public ServerStatus getStatus(String slug) {
        try {
            InspectContainerResponse info = docker.inspectContainerCmd(serverContainerName(slug)).exec();
            String state = info.getState().getStatus();
            String id = info.getId();
            if ("running".equals(state)) return ServerStatus.running(id);
            return ServerStatus.stopped(id);
        } catch (NotFoundException e) {
            return ServerStatus.notFound();
        } catch (Exception e) {
            return ServerStatus.error(e.getMessage());
        }
    }

    @Override
    public String getServerEndpoint(String slug) {
        return "http://" + serverContainerName(slug) + ":8081";
    }

    private void createServerContainer(TenantProvisionRequest req, String name, String tenantNetwork) {
        String slug = req.slug();
        String prefix = "/t/" + slug;

        // Traefik labels — need >10 entries, use HashMap
        var labels = new java.util.HashMap<String, String>();
        labels.put("traefik.enable", "true");
        labels.put("traefik.http.routers.server-" + slug + ".rule",
            "PathPrefix(`" + prefix + "/api`) || PathPrefix(`" + prefix + "/actuator`)");
        labels.put("traefik.http.routers.server-" + slug + ".tls", "true");
        labels.put("traefik.http.routers.server-" + slug + ".priority", "10");
        labels.put("traefik.http.middlewares.server-strip-" + slug + ".stripprefix.prefixes", prefix);
        labels.put("traefik.http.routers.server-" + slug + ".middlewares", "server-strip-" + slug);
        labels.put("traefik.http.services.server-" + slug + ".loadbalancer.server.port", "8081");
        labels.put("cameleer.tenant", slug);
        labels.put("cameleer.role", "server");

        List<String> env = List.of(
            "SPRING_DATASOURCE_URL=" + props.datasourceUrl(),
            "SPRING_DATASOURCE_USERNAME=cameleer",
            "SPRING_DATASOURCE_PASSWORD=cameleer_dev",
            "CLICKHOUSE_URL=jdbc:clickhouse://clickhouse:8123/cameleer",
            "CAMELEER_TENANT_ID=" + slug,
            "CAMELEER_AUTH_TOKEN=" + req.licenseToken(),
            "CAMELEER_JWT_SECRET=cameleer-dev-jwt-secret-change-in-production",
            "CAMELEER_OIDC_ISSUER_URI=" + props.oidcIssuerUri(),
            "CAMELEER_OIDC_JWK_SET_URI=" + props.oidcJwkSetUri(),
            "CAMELEER_OIDC_AUDIENCE=https://api.cameleer.local",
            "CAMELEER_OIDC_TLS_SKIP_VERIFY=true",
            "CAMELEER_CORS_ALLOWED_ORIGINS=" + props.corsOrigins(),
            "CAMELEER_LICENSE_TOKEN=" + req.licenseToken(),
            "CAMELEER_RUNTIME_ENABLED=true",
            "CAMELEER_SERVER_URL=http://" + name + ":8081",
            "CAMELEER_ROUTING_DOMAIN=" + props.publicHost(),
            "CAMELEER_ROUTING_MODE=path",
            "CAMELEER_JAR_STORAGE_PATH=/data/jars",
            // Apps deployed by this server join the tenant network (isolated)
            "CAMELEER_DOCKER_NETWORK=" + tenantNetwork,
            "CAMELEER_JAR_DOCKER_VOLUME=cameleer-jars-" + slug
        );

        // Primary network = tenant-isolated network
        HostConfig hostConfig = HostConfig.newHostConfig()
            .withRestartPolicy(RestartPolicy.unlessStoppedRestart())
            .withNetworkMode(tenantNetwork)
            .withBinds(new Bind("/var/run/docker.sock", new Volume("/var/run/docker.sock")))
            .withGroupAdd(List.of("0"));

        CreateContainerResponse resp = docker.createContainerCmd(props.serverImage())
            .withName(name)
            .withLabels(labels)
            .withEnv(env)
            .withHostConfig(hostConfig)
            .withHealthcheck(new HealthCheck()
                .withTest(List.of("CMD-SHELL", "curl -sf http://localhost:8081/api/v1/health || exit 1"))
                .withInterval(5_000_000_000L)
                .withTimeout(5_000_000_000L)
                .withRetries(30)
                .withStartPeriod(15_000_000_000L))
            .exec();

        String containerId = resp.getId();

        // Connect to shared services network (postgres, clickhouse, logto)
        docker.connectToNetworkCmd()
            .withNetworkId(props.networkName())
            .withContainerId(containerId)
            .withContainerNetwork(new ContainerNetwork().withAliases(List.of(name)))
            .exec();

        // Connect to traefik network for routing
        docker.connectToNetworkCmd()
            .withNetworkId(props.traefikNetwork())
            .withContainerId(containerId)
            .withContainerNetwork(new ContainerNetwork().withAliases(List.of(name)))
            .exec();
    }

    private void createUiContainer(String slug, String uiName, String serverName, String tenantNetwork) {
        String prefix = "/t/" + slug;

        var labels = new java.util.HashMap<String, String>();
        labels.put("traefik.enable", "true");
        labels.put("traefik.http.routers.ui-" + slug + ".rule", "PathPrefix(`" + prefix + "`)");
        labels.put("traefik.http.routers.ui-" + slug + ".tls", "true");
        labels.put("traefik.http.routers.ui-" + slug + ".priority", "5");
        labels.put("traefik.http.middlewares.ui-strip-" + slug + ".stripprefix.prefixes", prefix);
        labels.put("traefik.http.routers.ui-" + slug + ".middlewares", "ui-strip-" + slug);
        labels.put("traefik.http.services.ui-" + slug + ".loadbalancer.server.port", "80");
        labels.put("cameleer.tenant", slug);
        labels.put("cameleer.role", "server-ui");

        List<String> env = List.of(
            "BASE_PATH=" + prefix,
            "API_URL=http://" + serverName + ":8081"
        );

        // Primary network = tenant network (can reach server via DNS)
        HostConfig hostConfig = HostConfig.newHostConfig()
            .withRestartPolicy(RestartPolicy.unlessStoppedRestart())
            .withNetworkMode(tenantNetwork);

        CreateContainerResponse resp = docker.createContainerCmd(props.serverUiImage())
            .withName(uiName)
            .withLabels(labels)
            .withEnv(env)
            .withHostConfig(hostConfig)
            .exec();

        // Connect to traefik for routing
        docker.connectToNetworkCmd()
            .withNetworkId(props.traefikNetwork())
            .withContainerId(resp.getId())
            .exec();
    }

    private boolean waitForHealth(String containerName, int timeoutSeconds) {
        long deadline = System.currentTimeMillis() + timeoutSeconds * 1000L;
        while (System.currentTimeMillis() < deadline) {
            try {
                InspectContainerResponse info = docker.inspectContainerCmd(containerName).exec();
                InspectContainerResponse.ContainerState state = info.getState();
                if (state.getHealth() != null && "healthy".equals(state.getHealth().getStatus())) {
                    return true;
                }
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return false;
            } catch (Exception e) {
                log.debug("Health check poll for '{}': {}", containerName, e.getMessage());
                try { Thread.sleep(2000); } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    return false;
                }
            }
        }
        return false;
    }

    private void pullIfMissing(String image) {
        try {
            docker.inspectImageCmd(image).exec();
        } catch (NotFoundException e) {
            log.info("Pulling image: {}", image);
            try {
                docker.pullImageCmd(image).start().awaitCompletion();
            } catch (Exception ex) {
                log.warn("Failed to pull {}: {}", image, ex.getMessage());
            }
        }
    }

    private void stopIfRunning(String name) {
        try {
            docker.stopContainerCmd(name).withTimeout(30).exec();
        } catch (NotFoundException ignored) {}
    }

    private void removeContainer(String name) {
        try {
            docker.removeContainerCmd(name).withForce(true).exec();
        } catch (NotFoundException ignored) {}
    }

    private void ensureNetwork(String networkName) {
        try {
            docker.inspectNetworkCmd().withNetworkId(networkName).exec();
            log.debug("Network '{}' already exists", networkName);
        } catch (NotFoundException e) {
            docker.createNetworkCmd()
                .withName(networkName)
                .withDriver("bridge")
                .withInternal(true) // no external access — isolated
                .exec();
            log.info("Created isolated tenant network: {}", networkName);
        }
    }

    private void removeNetwork(String networkName) {
        try {
            docker.removeNetworkCmd(networkName).exec();
            log.info("Removed tenant network: {}", networkName);
        } catch (NotFoundException ignored) {
        } catch (Exception e) {
            log.warn("Failed to remove network '{}': {}", networkName, e.getMessage());
        }
    }

    private String serverContainerName(String slug) {
        return "cameleer-server-" + slug;
    }

    private String uiContainerName(String slug) {
        return "cameleer-server-ui-" + slug;
    }

    private String tenantNetworkName(String slug) {
        return "cameleer-tenant-" + slug;
    }
}
