package com.cameleer.server.app.runtime;

import com.cameleer.server.app.metrics.ServerMetrics;
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;

    @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.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) {
        this.orchestrator = orchestrator;
        this.deploymentService = deploymentService;
        this.appService = appService;
        this.envService = envService;
        this.deploymentRepository = deploymentRepository;
        this.pgDeployRepo = (PostgresDeploymentRepository) deploymentRepository;
    }

    @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());

            var globalDefaults = new ConfigMerger.GlobalRuntimeDefaults(
                    parseMemoryLimitMb(globalMemoryLimit),
                    globalCpuShares,
                    globalRoutingMode,
                    globalRoutingDomain,
                    globalServerUrl.isBlank() ? "http://cameleer-server:8081" : globalServerUrl
            );
            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;
            String envNet = null;
            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
                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);
                    }
                }
            }

            // === START REPLICAS ===
            updateStage(deployment.id(), DeployStage.START_REPLICAS);

            Map<String, String> baseEnvVars = buildEnvVars(app, env, config);
            Map<String, String> prometheusLabels = PrometheusLabelBuilder.build(resolvedRuntimeType);

            List<Map<String, Object>> replicaStates = new ArrayList<>();
            List<String> newContainerIds = new ArrayList<>();

            for (int i = 0; i < config.replicas(); i++) {
                String instanceId = env.slug() + "-" + app.slug() + "-" + i;
                String containerName = tenantId + "-" + instanceId;

                // Per-replica labels (include replica index and instance-id)
                Map<String, String> labels = TraefikLabelBuilder.build(app.slug(), env.slug(), tenantId, config, i);
                labels.putAll(prometheusLabels);

                // Per-replica env vars (set agent instance ID to match container log identity)
                Map<String, String> replicaEnvVars = new LinkedHashMap<>(baseEnvVars);
                replicaEnvVars.put("CAMELEER_AGENT_INSTANCEID", instanceId);

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

                String containerId = orchestrator.startContainer(request);
                newContainerIds.add(containerId);

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

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

                replicaStates.add(Map.of(
                        "index", i,
                        "containerId", containerId,
                        "containerName", containerName,
                        "status", "STARTING"
                ));
            }

            pgDeployRepo.updateReplicaStates(deployment.id(), replicaStates);

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

            if (healthyCount == 0) {
                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);
                deploymentService.markFailed(deployment.id(), "No replicas passed health check within " + healthCheckTimeout + "s");
                serverMetrics.recordDeploymentOutcome("FAILED");
                serverMetrics.recordDeploymentDuration(deployStart);
                return;
            }

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

            // === SWAP TRAFFIC ===
            updateStage(deployment.id(), DeployStage.SWAP_TRAFFIC);

            Optional<Deployment> existing = deploymentRepository.findActiveByAppIdAndEnvironmentId(
                    deployment.appId(), deployment.environmentId());
            if (existing.isPresent() && !existing.get().id().equals(deployment.id())) {
                stopDeploymentContainers(existing.get());
                deploymentService.markStopped(existing.get().id());
                log.info("Stopped previous deployment {} for replacement", existing.get().id());
            }

            // === COMPLETE ===
            updateStage(deployment.id(), DeployStage.COMPLETE);

            String primaryContainerId = newContainerIds.get(0);
            DeploymentStatus finalStatus = healthyCount == config.replicas()
                    ? DeploymentStatus.RUNNING : DeploymentStatus.DEGRADED;
            deploymentService.markRunning(deployment.id(), primaryContainerId);
            if (finalStatus == DeploymentStatus.DEGRADED) {
                deploymentRepository.updateStatus(deployment.id(), DeploymentStatus.DEGRADED,
                        primaryContainerId, null);
            }

            pgDeployRepo.updateDeployStage(deployment.id(), null);
            serverMetrics.recordDeploymentOutcome(finalStatus.name());
            serverMetrics.recordDeploymentDuration(deployStart);
            log.info("Deployment {} is {} ({}/{} replicas healthy)",
                    deployment.id(), finalStatus, healthyCount, config.replicas());

        } 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);
        }
    }

    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;
    }

    private int waitForAnyHealthy(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());
        return map;
    }
}
