package com.cameleer.server.app.runtime;

import com.cameleer.common.model.ApplicationConfig;
import com.cameleer.server.app.metrics.ServerMetrics;
import com.cameleer.server.app.storage.PostgresApplicationConfigRepository;
import com.cameleer.server.app.storage.PostgresDeploymentRepository;
import com.cameleer.server.core.runtime.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;

@Service
public class DeploymentExecutor {

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

    private final RuntimeOrchestrator orchestrator;
    private final DeploymentService deploymentService;
    private final AppService appService;
    private final EnvironmentService envService;
    private final DeploymentRepository deploymentRepository;
    private final PostgresDeploymentRepository pgDeployRepo;
    private final PostgresApplicationConfigRepository applicationConfigRepository;

    @Autowired(required = false)
    private DockerNetworkManager networkManager;

    @Value("${cameleer.server.runtime.baseimage:gitea.siegeln.net/cameleer/cameleer-runtime-base:latest}")
    private String baseImage;

    @Value("${cameleer.server.runtime.dockernetwork:cameleer}")
    private String dockerNetwork;

    @Value("${cameleer.server.runtime.container.memorylimit:512m}")
    private String globalMemoryLimit;

    @Value("${cameleer.server.runtime.container.cpushares:512}")
    private int globalCpuShares;

    @Value("${cameleer.server.runtime.healthchecktimeout:60}")
    private int healthCheckTimeout;

    @Value("${cameleer.server.runtime.agenthealthport:9464}")
    private int agentHealthPort;

    @Value("${cameleer.server.security.bootstraptoken:}")
    private String bootstrapToken;

    @Value("${cameleer.server.runtime.routingmode:path}")
    private String globalRoutingMode;

    @Value("${cameleer.server.runtime.routingdomain:localhost}")
    private String globalRoutingDomain;

    @Value("${cameleer.server.runtime.serverurl:}")
    private String globalServerUrl;

    @Value("${cameleer.server.runtime.certresolver:}")
    private String globalCertResolver;

    @Value("${cameleer.server.runtime.jardockervolume:}")
    private String jarDockerVolume;

    @Value("${cameleer.server.runtime.jarstoragepath:/data/jars}")
    private String jarStoragePath;

    @Value("${cameleer.server.tenant.id:default}")
    private String tenantId;

    @Autowired
    private ServerMetrics serverMetrics;

    public DeploymentExecutor(RuntimeOrchestrator orchestrator,
                              DeploymentService deploymentService,
                              AppService appService,
                              EnvironmentService envService,
                              DeploymentRepository deploymentRepository,
                              PostgresApplicationConfigRepository applicationConfigRepository) {
        this.orchestrator = orchestrator;
        this.deploymentService = deploymentService;
        this.appService = appService;
        this.envService = envService;
        this.deploymentRepository = deploymentRepository;
        this.pgDeployRepo = (PostgresDeploymentRepository) deploymentRepository;
        this.applicationConfigRepository = applicationConfigRepository;
    }

    /** Deployment-scoped id suffix — distinguishes container names and
     * CAMELEER_AGENT_INSTANCEID across redeploys so old + new replicas can
     * coexist during a blue/green swap. First 8 chars of the deployment UUID. */
    static String generationOf(Deployment deployment) {
        return deployment.id().toString().substring(0, 8);
    }

    /**
     * Per-deployment context assembled once at the top of executeAsync and passed
     * into strategy handlers. Keeps the strategy methods readable instead of
     * threading 12 positional args.
     */
    private record DeployCtx(
            Deployment deployment,
            App app,
            Environment env,
            ResolvedContainerConfig config,
            String jarPath,
            String resolvedRuntimeType,
            String mainClass,
            String generation,
            String primaryNetwork,
            List<String> additionalNets,
            Map<String, String> baseEnvVars,
            Map<String, String> prometheusLabels,
            long deployStart
    ) {}

    @Async("deploymentTaskExecutor")
    public void executeAsync(Deployment deployment) {
        long deployStart = System.currentTimeMillis();
        try {
            App app = appService.getById(deployment.appId());
            Environment env = envService.getById(deployment.environmentId());
            String jarPath = appService.resolveJarPath(deployment.appVersionId());
            String generation = generationOf(deployment);

            var globalDefaults = new ConfigMerger.GlobalRuntimeDefaults(
                    parseMemoryLimitMb(globalMemoryLimit),
                    globalCpuShares,
                    globalRoutingMode,
                    globalRoutingDomain,
                    globalServerUrl.isBlank() ? "http://cameleer-server:8081" : globalServerUrl,
                    globalCertResolver.isBlank() ? null : globalCertResolver
            );
            ResolvedContainerConfig config = ConfigMerger.resolve(
                    globalDefaults, env.defaultContainerConfig(), app.containerConfig());

            pgDeployRepo.updateDeploymentStrategy(deployment.id(), config.deploymentStrategy());
            pgDeployRepo.updateResolvedConfig(deployment.id(), resolvedConfigToMap(config));

            // === PRE-FLIGHT ===
            updateStage(deployment.id(), DeployStage.PRE_FLIGHT);
            preFlightChecks(jarPath, config);

            // Resolve runtime type
            String resolvedRuntimeType = config.runtimeType();
            String mainClass = null;
            if ("auto".equalsIgnoreCase(resolvedRuntimeType)) {
                AppVersion appVersion = appService.getVersion(deployment.appVersionId());
                if (appVersion.detectedRuntimeType() == null) {
                    throw new IllegalStateException(
                            "Could not detect runtime type for JAR '" + appVersion.jarFilename() +
                            "'. Set runtimeType explicitly in app configuration.");
                }
                resolvedRuntimeType = appVersion.detectedRuntimeType();
                mainClass = appVersion.detectedMainClass();
            } else if ("plain-java".equals(resolvedRuntimeType)) {
                AppVersion appVersion = appService.getVersion(deployment.appVersionId());
                mainClass = appVersion.detectedMainClass();
                if (mainClass == null) {
                    throw new IllegalStateException(
                            "Runtime type 'plain-java' requires a Main-Class in the JAR manifest, " +
                            "but none was detected for '" + appVersion.jarFilename() + "'.");
                }
            }

            // === PULL IMAGE ===
            updateStage(deployment.id(), DeployStage.PULL_IMAGE);
            orchestrator.pullImage(baseImage);

            // === CREATE NETWORKS ===
            updateStage(deployment.id(), DeployStage.CREATE_NETWORK);
            // Primary network: use configured CAMELEER_DOCKER_NETWORK (tenant-isolated in SaaS mode)
            String primaryNetwork = dockerNetwork;
            List<String> additionalNets = new ArrayList<>();
            if (networkManager != null) {
                networkManager.ensureNetwork(primaryNetwork);
                // Traefik network for routing (apps need to be reachable by Traefik)
                networkManager.ensureNetwork(DockerNetworkManager.TRAEFIK_NETWORK);
                additionalNets.add(DockerNetworkManager.TRAEFIK_NETWORK);
                // Per-environment network scoped to tenant to prevent cross-tenant collisions
                String envNet = DockerNetworkManager.envNetworkName(tenantId, env.slug());
                networkManager.ensureNetwork(envNet);
                additionalNets.add(envNet);
            }

            // User-configured extra networks (e.g., monitoring)
            if (networkManager != null && config.extraNetworks() != null) {
                for (String net : config.extraNetworks()) {
                    if (!net.isBlank() && !additionalNets.contains(net)) {
                        networkManager.ensureNetwork(net);
                        additionalNets.add(net);
                    }
                }
            }

            DeployCtx ctx = new DeployCtx(
                    deployment, app, env, config, jarPath,
                    resolvedRuntimeType, mainClass, generation,
                    primaryNetwork, additionalNets,
                    buildEnvVars(app, env, config),
                    PrometheusLabelBuilder.build(resolvedRuntimeType),
                    deployStart);

            // Dispatch on strategy. Unknown values fall back to BLUE_GREEN via fromWire.
            DeploymentStrategy strategy = DeploymentStrategy.fromWire(config.deploymentStrategy());
            switch (strategy) {
                case BLUE_GREEN -> deployBlueGreen(ctx);
                case ROLLING -> deployRolling(ctx);
            }

        } catch (Exception e) {
            log.error("Deployment {} FAILED: {}", deployment.id(), e.getMessage(), e);
            pgDeployRepo.updateDeployStage(deployment.id(), null);
            deploymentService.markFailed(deployment.id(), e.getMessage());
            serverMetrics.recordDeploymentOutcome("FAILED");
            serverMetrics.recordDeploymentDuration(deployStart);
        }
    }

    /**
     * Blue/green strategy: start all N new replicas (coexisting with the old
     * ones thanks to the gen-suffixed container names), wait for ALL healthy,
     * then stop the previous deployment. Strict all-healthy — partial failure
     * preserves the previous deployment untouched.
     */
    private void deployBlueGreen(DeployCtx ctx) {
        ResolvedContainerConfig config = ctx.config();
        Deployment deployment = ctx.deployment();

        // === START REPLICAS ===
        updateStage(deployment.id(), DeployStage.START_REPLICAS);
        List<Map<String, Object>> replicaStates = new ArrayList<>();
        List<String> newContainerIds = new ArrayList<>();
        for (int i = 0; i < config.replicas(); i++) {
            Map<String, Object> state = new LinkedHashMap<>();
            String containerId = startReplica(ctx, i, state);
            newContainerIds.add(containerId);
            replicaStates.add(state);
        }
        pgDeployRepo.updateReplicaStates(deployment.id(), replicaStates);

        // === HEALTH CHECK ===
        updateStage(deployment.id(), DeployStage.HEALTH_CHECK);
        int healthyCount = waitForAllHealthy(newContainerIds, healthCheckTimeout);

        if (healthyCount < config.replicas()) {
            // Strict abort: tear down new replicas, leave the previous deployment untouched.
            for (String cid : newContainerIds) {
                try { orchestrator.stopContainer(cid); orchestrator.removeContainer(cid); }
                catch (Exception e) { log.warn("Cleanup failed for {}: {}", cid, e.getMessage()); }
            }
            pgDeployRepo.updateDeployStage(deployment.id(), null);
            String reason = String.format(
                    "blue-green: %d/%d replicas healthy within %ds; preserving previous deployment",
                    healthyCount, config.replicas(), healthCheckTimeout);
            deploymentService.markFailed(deployment.id(), reason);
            serverMetrics.recordDeploymentOutcome("FAILED");
            serverMetrics.recordDeploymentDuration(ctx.deployStart());
            return;
        }

        replicaStates = updateReplicaHealth(replicaStates, newContainerIds);
        pgDeployRepo.updateReplicaStates(deployment.id(), replicaStates);

        // === SWAP TRAFFIC ===
        // All new replicas are healthy; Traefik labels are already attracting
        // traffic to them. Stop the previous deployment now — the swap is
        // implicit in the label-driven load balancer.
        updateStage(deployment.id(), DeployStage.SWAP_TRAFFIC);
        Optional<Deployment> previous = deploymentRepository.findActiveByAppIdAndEnvironmentIdExcluding(
                deployment.appId(), deployment.environmentId(), deployment.id());
        if (previous.isPresent()) {
            log.info("blue-green: stopping previous deployment {} now that new replicas are healthy",
                    previous.get().id());
            stopDeploymentContainers(previous.get());
            deploymentService.markStopped(previous.get().id());
        }

        // === COMPLETE ===
        updateStage(deployment.id(), DeployStage.COMPLETE);
        persistSnapshotAndMarkRunning(ctx, newContainerIds.get(0));
        log.info("Deployment {} is RUNNING (blue-green, {}/{} replicas healthy)",
                deployment.id(), healthyCount, config.replicas());
    }

    /**
     * Rolling strategy: replace replicas one at a time — start new[i], wait
     * healthy, stop old[i]. On any replica's health failure, stop the
     * in-flight new container, leave remaining old replicas serving, mark
     * FAILED. Already-replaced old containers are not restored (can't unring
     * that bell) — user redeploys to recover.
     *
     * Resource peak: replicas + 1 (briefly while a new replica warms up
     * before its counterpart is stopped).
     */
    private void deployRolling(DeployCtx ctx) {
        ResolvedContainerConfig config = ctx.config();
        Deployment deployment = ctx.deployment();

        // Capture previous deployment's per-index container ids up front.
        Optional<Deployment> previousOpt = deploymentRepository.findActiveByAppIdAndEnvironmentIdExcluding(
                deployment.appId(), deployment.environmentId(), deployment.id());
        Map<Integer, String> oldContainerByIndex = new LinkedHashMap<>();
        if (previousOpt.isPresent() && previousOpt.get().replicaStates() != null) {
            for (Map<String, Object> r : previousOpt.get().replicaStates()) {
                Object idx = r.get("index");
                Object cid = r.get("containerId");
                if (idx instanceof Number n && cid instanceof String s) {
                    oldContainerByIndex.put(n.intValue(), s);
                }
            }
        }

        // === START REPLICAS ===
        updateStage(deployment.id(), DeployStage.START_REPLICAS);
        List<Map<String, Object>> replicaStates = new ArrayList<>();
        List<String> newContainerIds = new ArrayList<>();

        for (int i = 0; i < config.replicas(); i++) {
            // Start new replica i (gen-suffixed name; coexists with old[i]).
            Map<String, Object> state = new LinkedHashMap<>();
            String newCid = startReplica(ctx, i, state);
            newContainerIds.add(newCid);
            replicaStates.add(state);
            pgDeployRepo.updateReplicaStates(deployment.id(), replicaStates);

            // === HEALTH CHECK (per-replica) ===
            updateStage(deployment.id(), DeployStage.HEALTH_CHECK);
            boolean healthy = waitForOneHealthy(newCid, healthCheckTimeout);
            if (!healthy) {
                // Abort: stop this in-flight new replica AND any new replicas
                // started so far. Already-stopped old replicas stay stopped
                // (rolling is not reversible). Remaining un-replaced old
                // replicas keep serving traffic.
                for (String cid : newContainerIds) {
                    try { orchestrator.stopContainer(cid); orchestrator.removeContainer(cid); }
                    catch (Exception e) { log.warn("Cleanup failed for {}: {}", cid, e.getMessage()); }
                }
                pgDeployRepo.updateDeployStage(deployment.id(), null);
                String reason = String.format(
                        "rolling: replica %d failed to reach healthy within %ds; %d previous replicas still running",
                        i, healthCheckTimeout, oldContainerByIndex.size());
                deploymentService.markFailed(deployment.id(), reason);
                serverMetrics.recordDeploymentOutcome("FAILED");
                serverMetrics.recordDeploymentDuration(ctx.deployStart());
                return;
            }

            // Health check passed: update replica status to RUNNING, stop the
            // corresponding old[i] if present, and continue with replica i+1.
            replicaStates = updateReplicaHealth(replicaStates, newContainerIds);
            pgDeployRepo.updateReplicaStates(deployment.id(), replicaStates);

            String oldCid = oldContainerByIndex.remove(i);
            if (oldCid != null) {
                try {
                    orchestrator.stopContainer(oldCid);
                    orchestrator.removeContainer(oldCid);
                    log.info("rolling: replaced replica {} (old={}, new={})", i, oldCid, newCid);
                } catch (Exception e) {
                    log.warn("rolling: failed to stop old replica {} ({}): {}", i, oldCid, e.getMessage());
                }
            }
        }

        // === SWAP TRAFFIC ===
        // Any old replicas with indices >= new.replicas (e.g., when replica
        // count shrank) are still running; sweep them now so the old
        // deployment can be marked STOPPED.
        updateStage(deployment.id(), DeployStage.SWAP_TRAFFIC);
        for (Map.Entry<Integer, String> e : oldContainerByIndex.entrySet()) {
            try {
                orchestrator.stopContainer(e.getValue());
                orchestrator.removeContainer(e.getValue());
                log.info("rolling: stopped leftover old replica {} ({})", e.getKey(), e.getValue());
            } catch (Exception ex) {
                log.warn("rolling: failed to stop leftover old replica {}: {}", e.getKey(), ex.getMessage());
            }
        }
        if (previousOpt.isPresent()) {
            deploymentService.markStopped(previousOpt.get().id());
        }

        // === COMPLETE ===
        updateStage(deployment.id(), DeployStage.COMPLETE);
        persistSnapshotAndMarkRunning(ctx, newContainerIds.get(0));
        log.info("Deployment {} is RUNNING (rolling, {}/{} replicas replaced)",
                deployment.id(), config.replicas(), config.replicas());
    }

    /** Poll a single container until healthy or the timeout expires. Returns
     * true on healthy, false on timeout or thread interrupt. */
    private boolean waitForOneHealthy(String containerId, int timeoutSeconds) {
        long deadline = System.currentTimeMillis() + (timeoutSeconds * 1000L);
        while (System.currentTimeMillis() < deadline) {
            ContainerStatus status = orchestrator.getContainerStatus(containerId);
            if ("healthy".equals(status.state())) return true;
            try { Thread.sleep(2000); } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return false;
            }
        }
        return false;
    }

    /** Start one replica container with the gen-suffixed name and return its
     * container id. Fills `stateOut` with the replicaStates JSONB row. */
    private String startReplica(DeployCtx ctx, int i, Map<String, Object> stateOut) {
        Environment env = ctx.env();
        App app = ctx.app();
        ResolvedContainerConfig config = ctx.config();

        String instanceId = env.slug() + "-" + app.slug() + "-" + i + "-" + ctx.generation();
        String containerName = tenantId + "-" + instanceId;

        Map<String, String> labels = TraefikLabelBuilder.build(
                app.slug(), env.slug(), tenantId, config, i, ctx.generation());
        labels.putAll(ctx.prometheusLabels());

        Map<String, String> replicaEnvVars = new LinkedHashMap<>(ctx.baseEnvVars());
        replicaEnvVars.put("CAMELEER_AGENT_INSTANCEID", instanceId);

        String volumeName = jarDockerVolume != null && !jarDockerVolume.isBlank() ? jarDockerVolume : null;
        ContainerRequest request = new ContainerRequest(
                containerName, baseImage, ctx.jarPath(),
                volumeName, jarStoragePath,
                ctx.primaryNetwork(),
                ctx.additionalNets(),
                replicaEnvVars, labels,
                config.memoryLimitBytes(), config.memoryReserveBytes(),
                config.dockerCpuShares(), config.dockerCpuQuota(),
                config.exposedPorts(), agentHealthPort,
                "on-failure", 3,
                ctx.resolvedRuntimeType(), config.customArgs(), ctx.mainClass()
        );

        String containerId = orchestrator.startContainer(request);

        // Connect to additional networks after container is started
        for (String net : ctx.additionalNets()) {
            if (networkManager != null) {
                networkManager.connectContainer(containerId, net);
            }
        }

        orchestrator.startLogCapture(containerId, instanceId, app.slug(), env.slug(), tenantId);

        stateOut.put("index", i);
        stateOut.put("containerId", containerId);
        stateOut.put("containerName", containerName);
        stateOut.put("status", "STARTING");
        return containerId;
    }

    /** Persist the deployment snapshot and mark the deployment RUNNING.
     * Finalizes the deploy in a single place shared by all strategy paths. */
    private void persistSnapshotAndMarkRunning(DeployCtx ctx, String primaryContainerId) {
        Deployment deployment = ctx.deployment();
        ApplicationConfig agentConfig = applicationConfigRepository
                .findByApplicationAndEnvironment(ctx.app().slug(), ctx.env().slug())
                .orElse(null);
        List<String> snapshotSensitiveKeys = agentConfig != null ? agentConfig.getSensitiveKeys() : null;
        DeploymentConfigSnapshot snapshot = new DeploymentConfigSnapshot(
                deployment.appVersionId(),
                agentConfig,
                ctx.app().containerConfig(),
                snapshotSensitiveKeys);
        pgDeployRepo.saveDeployedConfigSnapshot(deployment.id(), snapshot);

        deploymentService.markRunning(deployment.id(), primaryContainerId);
        pgDeployRepo.updateDeployStage(deployment.id(), null);
        serverMetrics.recordDeploymentOutcome("RUNNING");
        serverMetrics.recordDeploymentDuration(ctx.deployStart());
    }

    public void stopDeployment(Deployment deployment) {
        pgDeployRepo.updateTargetState(deployment.id(), "STOPPED");
        deploymentRepository.updateStatus(deployment.id(), DeploymentStatus.STOPPING,
                deployment.containerId(), null);

        stopDeploymentContainers(deployment);
        deploymentService.markStopped(deployment.id());
    }

    private void stopDeploymentContainers(Deployment deployment) {
        List<Map<String, Object>> replicas = deployment.replicaStates() != null
                ? deployment.replicaStates() : List.of();
        for (Map<String, Object> replica : replicas) {
            String cid = (String) replica.get("containerId");
            if (cid != null) {
                try {
                    orchestrator.stopContainer(cid);
                    orchestrator.removeContainer(cid);
                } catch (Exception e) {
                    log.warn("Failed to stop replica container {}: {}", cid, e.getMessage());
                }
            }
        }
        if (deployment.containerId() != null && replicas.isEmpty()) {
            try {
                orchestrator.stopContainer(deployment.containerId());
                orchestrator.removeContainer(deployment.containerId());
            } catch (Exception e) {
                log.warn("Failed to stop container {}: {}", deployment.containerId(), e.getMessage());
            }
        }
    }

    private void preFlightChecks(String jarPath, ResolvedContainerConfig config) {
        if (!Files.exists(Path.of(jarPath))) {
            throw new IllegalStateException("JAR file not found: " + jarPath);
        }
        if (config.memoryLimitMb() <= 0) {
            throw new IllegalStateException("Memory limit must be positive, got: " + config.memoryLimitMb());
        }
        if (config.appPort() <= 0 || config.appPort() > 65535) {
            throw new IllegalStateException("Invalid app port: " + config.appPort());
        }
        if (config.replicas() < 1) {
            throw new IllegalStateException("Replicas must be >= 1, got: " + config.replicas());
        }
    }

    private Map<String, String> buildEnvVars(App app, Environment env, ResolvedContainerConfig config) {
        Map<String, String> envVars = new LinkedHashMap<>();
        envVars.put("CAMELEER_AGENT_EXPORT_TYPE", "HTTP");
        envVars.put("CAMELEER_AGENT_APPLICATION", app.slug());
        envVars.put("CAMELEER_AGENT_ENVIRONMENT", env.slug());
        envVars.put("CAMELEER_AGENT_EXPORT_ENDPOINT", config.serverUrl());
        envVars.put("CAMELEER_AGENT_ROUTECONTROL_ENABLED", String.valueOf(config.routeControlEnabled()));
        envVars.put("CAMELEER_AGENT_REPLAY_ENABLED", String.valueOf(config.replayEnabled()));
        envVars.put("CAMELEER_AGENT_HEALTH_ENABLED", "true");
        envVars.put("CAMELEER_AGENT_HEALTH_PORT", String.valueOf(agentHealthPort));
        if (bootstrapToken != null && !bootstrapToken.isBlank()) {
            envVars.put("CAMELEER_AGENT_AUTH_TOKEN", bootstrapToken);
        }
        envVars.putAll(config.customEnvVars());
        return envVars;
    }

    /** Poll until all containers are healthy or the timeout expires. Returns
     * the healthy count at return time — == ids.size() on full success, less
     * if the timeout won. */
    private int waitForAllHealthy(List<String> containerIds, int timeoutSeconds) {
        long deadline = System.currentTimeMillis() + (timeoutSeconds * 1000L);
        int lastHealthy = 0;
        while (System.currentTimeMillis() < deadline) {
            int healthy = 0;
            for (String cid : containerIds) {
                ContainerStatus status = orchestrator.getContainerStatus(cid);
                if ("healthy".equals(status.state())) healthy++;
            }
            lastHealthy = healthy;
            if (healthy == containerIds.size()) return healthy;
            try { Thread.sleep(2000); } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return lastHealthy;
            }
        }
        return lastHealthy;
    }

    private List<Map<String, Object>> updateReplicaHealth(List<Map<String, Object>> replicas,
                                                           List<String> containerIds) {
        List<Map<String, Object>> updated = new ArrayList<>();
        for (Map<String, Object> replica : replicas) {
            String cid = (String) replica.get("containerId");
            ContainerStatus status = orchestrator.getContainerStatus(cid);
            Map<String, Object> copy = new HashMap<>(replica);
            copy.put("status", status.running() ? "RUNNING" : "DEAD");
            updated.add(copy);
        }
        return updated;
    }

    private void updateStage(UUID deploymentId, DeployStage stage) {
        pgDeployRepo.updateDeployStage(deploymentId, stage.name());
    }

    private int parseMemoryLimitMb(String limit) {
        limit = limit.trim().toLowerCase();
        if (limit.endsWith("g")) return (int) (Double.parseDouble(limit.replace("g", "")) * 1024);
        if (limit.endsWith("m")) return (int) Double.parseDouble(limit.replace("m", ""));
        return Integer.parseInt(limit);
    }

    private Map<String, Object> resolvedConfigToMap(ResolvedContainerConfig config) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("memoryLimitMb", config.memoryLimitMb());
        if (config.memoryReserveMb() != null) map.put("memoryReserveMb", config.memoryReserveMb());
        map.put("cpuRequest", config.cpuRequest());
        if (config.cpuLimit() != null) map.put("cpuLimit", config.cpuLimit());
        map.put("appPort", config.appPort());
        map.put("exposedPorts", config.exposedPorts());
        map.put("customEnvVars", config.customEnvVars());
        map.put("stripPathPrefix", config.stripPathPrefix());
        map.put("sslOffloading", config.sslOffloading());
        map.put("routingMode", config.routingMode());
        map.put("routingDomain", config.routingDomain());
        map.put("serverUrl", config.serverUrl());
        map.put("replicas", config.replicas());
        map.put("deploymentStrategy", config.deploymentStrategy());
        map.put("runtimeType", config.runtimeType());
        map.put("customArgs", config.customArgs());
        map.put("extraNetworks", config.extraNetworks());
        map.put("externalRouting", config.externalRouting());
        if (config.certResolver() != null) {
            map.put("certResolver", config.certResolver());
        }
        return map;
    }
}
