package com.cameleer.server.core.runtime;

import com.cameleer.common.model.ApplicationConfig;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;

/**
 * Compares the app's current desired state (JAR + agent config + container config) to the
 * config snapshot from the last successful deployment, producing a structured dirty result.
 *
 * <p>Pure logic — no IO, no Spring. Safe to unit-test as a POJO.
 * Caller must supply an {@link ObjectMapper} configured with {@code JavaTimeModule} so that
 * {@code ApplicationConfig.updatedAt} (an {@link java.time.Instant}) serialises correctly.</p>
 */
public class DirtyStateCalculator {

    // Live-pushed fields are excluded from the deploy diff: changes to them take effect
    // via SSE config-update without a redeploy, so they are not "pending deploy" when they
    // differ from the last successful deployment snapshot. See ui/rules: the Traces & Taps
    // and Route Recording tabs apply with ?apply=live and "never mark dirty".
    private static final Set<String> AGENT_CONFIG_IGNORED_KEYS = Set.of(
            "version", "updatedAt", "updatedBy", "environment", "application",
            "taps", "tapVersion", "tracedProcessors", "routeRecording"
    );

    private final ObjectMapper mapper;

    public DirtyStateCalculator(ObjectMapper mapper) {
        this.mapper = mapper;
    }

    private JsonNode scrubAgentConfig(JsonNode node) {
        if (!(node instanceof ObjectNode obj)) return node;
        ObjectNode copy = obj.deepCopy();
        for (String k : AGENT_CONFIG_IGNORED_KEYS) copy.remove(k);
        return copy;
    }

    public DirtyStateResult compute(UUID desiredJarVersionId,
                                    ApplicationConfig desiredAgentConfig,
                                    Map<String, Object> desiredContainerConfig,
                                    DeploymentConfigSnapshot snapshot) {
        List<DirtyStateResult.Difference> diffs = new ArrayList<>();

        if (snapshot == null) {
            diffs.add(new DirtyStateResult.Difference("snapshot", "(none)", "(none)"));
            return new DirtyStateResult(true, diffs);
        }

        if (!Objects.equals(desiredJarVersionId, snapshot.jarVersionId())) {
            diffs.add(new DirtyStateResult.Difference("jarVersionId",
                    String.valueOf(desiredJarVersionId), String.valueOf(snapshot.jarVersionId())));
        }

        compareJson("agentConfig",
                scrubAgentConfig(mapper.valueToTree(desiredAgentConfig)),
                scrubAgentConfig(mapper.valueToTree(snapshot.agentConfig())),
                diffs);
        compareJson("containerConfig", mapper.valueToTree(desiredContainerConfig),
                mapper.valueToTree(snapshot.containerConfig()), diffs);

        return new DirtyStateResult(!diffs.isEmpty(), diffs);
    }

    private void compareJson(String prefix, JsonNode desired, JsonNode deployed,
                             List<DirtyStateResult.Difference> diffs) {
        if (!(desired instanceof ObjectNode desiredObj) || !(deployed instanceof ObjectNode deployedObj)) {
            if (!Objects.equals(desired, deployed)) {
                diffs.add(new DirtyStateResult.Difference(prefix,
                        nodeToString(desired), nodeToString(deployed)));
            }
            return;
        }
        TreeSet<String> keys = new TreeSet<>();
        desiredObj.fieldNames().forEachRemaining(keys::add);
        deployedObj.fieldNames().forEachRemaining(keys::add);
        for (String key : keys) {
            JsonNode d = desiredObj.get(key);
            JsonNode p = deployedObj.get(key);
            if (Objects.equals(d, p)) continue;
            if (d instanceof ObjectNode && p instanceof ObjectNode) {
                compareJson(prefix + "." + key, d, p, diffs);
            } else {
                diffs.add(new DirtyStateResult.Difference(prefix + "." + key, nodeToString(d), nodeToString(p)));
            }
        }
    }

    private static String nodeToString(JsonNode n) {
        if (n == null) return "(none)";
        if (n.isValueNode()) return n.asText();
        return n.toString();  // arrays/objects: compact JSON
    }
}
