package com.cameleer.server.app.runtime;

import com.cameleer.server.app.storage.PostgresDeploymentRepository;
import com.cameleer.server.core.runtime.ContainerStatus;
import com.cameleer.server.core.runtime.Deployment;
import com.cameleer.server.core.runtime.DeploymentStatus;
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.Event;
import com.github.dockerjava.api.model.EventType;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;

import java.io.Closeable;
import java.io.IOException;
import java.util.*;

public class DockerEventMonitor {

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

    private final DockerClient dockerClient;
    private final RuntimeOrchestrator runtimeOrchestrator;
    private final PostgresDeploymentRepository deploymentRepository;
    private Closeable eventStream;

    public DockerEventMonitor(DockerRuntimeOrchestrator orchestrator,
                              PostgresDeploymentRepository deploymentRepository) {
        this.dockerClient = orchestrator.getDockerClient();
        this.runtimeOrchestrator = orchestrator;
        this.deploymentRepository = deploymentRepository;
    }

    @PostConstruct
    public void startListening() {
        eventStream = dockerClient.eventsCmd()
                .withEventTypeFilter(EventType.CONTAINER)
                .withEventFilter("die", "oom", "start", "stop")
                .exec(new ResultCallback.Adapter<Event>() {
                    @Override
                    public void onNext(Event event) {
                        handleEvent(event);
                    }

                    @Override
                    public void onError(Throwable throwable) {
                        log.warn("Docker event stream error, reconnecting: {}", throwable.getMessage());
                        reconnect();
                    }
                });

        log.info("Docker event monitor started");
    }

    @PreDestroy
    public void stop() {
        if (eventStream != null) {
            try { eventStream.close(); } catch (IOException e) { /* ignore */ }
        }
    }

    private void handleEvent(Event event) {
        String containerId = event.getId();
        if (containerId == null) return;

        Map<String, String> labels = event.getActor() != null ? event.getActor().getAttributes() : null;
        if (labels == null || !"cameleer-server".equals(labels.get("managed-by"))) return;

        String action = event.getAction();
        log.debug("Docker event: {} for container {} ({})", action, containerId.substring(0, 12),
                labels.get("cameleer.app"));

        Optional<Deployment> deploymentOpt = deploymentRepository.findByContainerId(containerId);
        if (deploymentOpt.isEmpty()) return;

        Deployment deployment = deploymentOpt.get();
        List<Map<String, Object>> replicas = new ArrayList<>(deployment.replicaStates());

        boolean changed = false;
        for (int i = 0; i < replicas.size(); i++) {
            Map<String, Object> replica = replicas.get(i);
            if (containerId.equals(replica.get("containerId"))) {
                Map<String, Object> updated = new HashMap<>(replica);
                switch (action) {
                    case "die", "oom", "stop" -> {
                        updated.put("status", "DEAD");
                        if ("oom".equals(action)) {
                            updated.put("oomKilled", true);
                            log.warn("Container {} OOM-killed (app={}, env={})", containerId.substring(0, 12),
                                    labels.get("cameleer.app"), labels.get("cameleer.environment"));
                        }
                    }
                    case "start" -> updated.put("status", "RUNNING");
                }
                replicas.set(i, updated);
                changed = true;
                // Stop log capture for this container — it's dead or stopped
                if ("die".equals(action) || "oom".equals(action) || "stop".equals(action)) {
                    runtimeOrchestrator.stopLogCapture(containerId);
                }
                break;
            }
        }

        if (!changed) return;

        deploymentRepository.updateReplicaStates(deployment.id(), replicas);

        long running = replicas.stream().filter(r -> "RUNNING".equals(r.get("status"))).count();
        DeploymentStatus newStatus;
        if (running == replicas.size()) {
            newStatus = DeploymentStatus.RUNNING;
        } else if (running > 0) {
            newStatus = DeploymentStatus.DEGRADED;
        } else {
            newStatus = DeploymentStatus.FAILED;
        }

        if (deployment.status() != newStatus) {
            deploymentRepository.updateStatus(deployment.id(), newStatus, deployment.containerId(), deployment.errorMessage());
            log.info("Deployment {} status: {} -> {} ({}/{} replicas running)",
                    deployment.id(), deployment.status(), newStatus, running, replicas.size());
        }
    }

    /**
     * Periodic reconciliation: inspects actual container state for active deployments
     * and corrects status mismatches caused by missed Docker events.
     */
    @Scheduled(fixedDelay = 30_000, initialDelay = 60_000)
    public void reconcile() {
        List<Deployment> active = deploymentRepository.findByStatus(
                List.of(DeploymentStatus.RUNNING, DeploymentStatus.DEGRADED, DeploymentStatus.STARTING));

        for (Deployment deployment : active) {
            if (deployment.replicaStates() == null || deployment.replicaStates().isEmpty()) continue;

            List<Map<String, Object>> replicas = new ArrayList<>(deployment.replicaStates());
            boolean changed = false;

            for (int i = 0; i < replicas.size(); i++) {
                Map<String, Object> replica = replicas.get(i);
                String containerId = (String) replica.get("containerId");
                if (containerId == null) continue;

                ContainerStatus actual = runtimeOrchestrator.getContainerStatus(containerId);
                String currentStatus = (String) replica.get("status");
                String actualStatus = actual.running() ? "RUNNING" : "DEAD";

                if (!actualStatus.equals(currentStatus)) {
                    Map<String, Object> updated = new HashMap<>(replica);
                    updated.put("status", actualStatus);
                    replicas.set(i, updated);
                    changed = true;
                }
            }

            if (!changed) {
                // Even if replica states haven't changed, check if deployment status is correct
                long running = replicas.stream().filter(r -> "RUNNING".equals(r.get("status"))).count();
                DeploymentStatus expected = running == replicas.size() ? DeploymentStatus.RUNNING
                        : running > 0 ? DeploymentStatus.DEGRADED : DeploymentStatus.FAILED;
                if (deployment.status() != expected) {
                    deploymentRepository.updateStatus(deployment.id(), expected, deployment.containerId(), deployment.errorMessage());
                    log.info("Reconcile: deployment {} status corrected {} -> {} ({}/{} running)",
                            deployment.id(), deployment.status(), expected, running, replicas.size());
                }
                continue;
            }

            deploymentRepository.updateReplicaStates(deployment.id(), replicas);

            long running = replicas.stream().filter(r -> "RUNNING".equals(r.get("status"))).count();
            DeploymentStatus newStatus = running == replicas.size() ? DeploymentStatus.RUNNING
                    : running > 0 ? DeploymentStatus.DEGRADED : DeploymentStatus.FAILED;

            if (deployment.status() != newStatus) {
                deploymentRepository.updateStatus(deployment.id(), newStatus, deployment.containerId(), deployment.errorMessage());
                log.info("Reconcile: deployment {} status {} -> {} ({}/{} replicas running)",
                        deployment.id(), deployment.status(), newStatus, running, replicas.size());
            }
        }
    }

    private void reconnect() {
        try {
            Thread.sleep(5000);
            startListening();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}
