package com.customer.work.service;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.flowable.engine.delegate.DelegateExecution;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Flowable Spring Boot component for tracking process variable changes across calls.
 *
 * Usage in Flowable expression:
 *   ${variableTracker.track(execution, 'order.customer.name,order.total,status')}
 *
 * Returns a JSON string with an array of changed variables, each entry containing:
 *   { "path": "order.customer.name", "oldValue": "Alice", "newValue": "Bob" }
 *
 * State (the snapshot of previous values) is stored as a process variable keyed by
 * SNAPSHOT_VAR_PREFIX + the provided paths string, so independent track() calls
 * with different path sets do not interfere with each other.
 */
@Component("variableTracker")
public class VariableTrackerComponent {

    private static final Logger LOGGER = LoggerFactory.getLogger(VariableTrackerComponent.class);
    private static final String SNAPSHOT_VAR_PREFIX = "__vartracker__";

    private final ObjectMapper objectMapper;

    public VariableTrackerComponent() {
        this.objectMapper = new ObjectMapper();
        this.objectMapper.registerModule(new JavaTimeModule());
        this.objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
    }

    /**
     * Checks which of the given variable paths changed since the last call and returns
     * a JSON array describing each change.
     *
     * @param execution   the current Flowable execution context
     * @param pathsCsv    comma-separated variable paths, e.g. "order.total,status,user.address.city"
     * @return JSON string — an array of { path, oldValue, newValue } objects for every changed path
     */
    public String track(DelegateExecution execution, String pathsCsv) {
        if (pathsCsv == null || pathsCsv.isBlank()) {
            return "[]";
        }

        List<String> paths = Arrays.stream(pathsCsv.split(","))
                .map(String::trim)
                .filter(s -> !s.isEmpty())
                .toList();

        String snapshotKey = SNAPSHOT_VAR_PREFIX + pathsCsv.replaceAll("\\s+", "");
        Map<String, JsonNode> previousSnapshot = loadSnapshot(execution, snapshotKey);
        Map<String, JsonNode> currentSnapshot = new HashMap<>();

        ArrayNode changes = objectMapper.createArrayNode();

        for (String path : paths) {
            JsonNode currentValue = resolvePathToJson(execution, path);
            currentSnapshot.put(path, currentValue);

            if (previousSnapshot.isEmpty()) {
                // First call — record baseline, no changes reported
                continue;
            }

            JsonNode previousValue = previousSnapshot.getOrDefault(path, objectMapper.nullNode());
            if (!jsonEquals(previousValue, currentValue)) {
                ObjectNode change = objectMapper.createObjectNode();
                change.put("path", path);
                change.set("oldValue", previousValue);
                change.set("newValue", currentValue);
                changes.add(change);
            }
        }

        saveSnapshot(execution, snapshotKey, currentSnapshot);

        try {
            return objectMapper.writeValueAsString(changes);
        } catch (Exception e) {
            LOGGER.error("Failed to serialize changes to JSON", e);
            return "[]";
        }
    }

    // -------------------------------------------------------------------------
    // Variable path resolution
    // -------------------------------------------------------------------------

    /**
     * Resolves a dot-separated path against the execution's variables and returns
     * the result as a JsonNode. The first segment is the top-level process variable
     * name; subsequent segments navigate into the object graph.
     */
    private JsonNode resolvePathToJson(DelegateExecution execution, String path) {
        String[] segments = path.split("\\.", -1);
        Object current = execution.getVariable(segments[0]);

        for (int i = 1; i < segments.length; i++) {
            if (current == null) {
                return objectMapper.nullNode();
            }
            current = getProperty(current, segments[i]);
        }

        return toJsonNode(current);
    }

    /**
     * Reads a named property from an object. Supports Maps, Jackson ObjectNodes,
     * and plain Java objects (via public getter or public field).
     */
    @SuppressWarnings("unchecked")
    private Object getProperty(Object obj, String property) {
        if (obj instanceof Map<?, ?> map) {
            return map.get(property);
        }
        if (obj instanceof ObjectNode on) {
            JsonNode node = on.get(property);
            return node != null ? node : null;
        }
        if (obj instanceof JsonNode jn) {
            JsonNode node = jn.get(property);
            return node != null ? node : null;
        }
        // Reflection: try getter first, then field
        try {
            String getter = "get" + Character.toUpperCase(property.charAt(0)) + property.substring(1);
            Method method = obj.getClass().getMethod(getter);
            return method.invoke(obj);
        } catch (Exception ignored) {
        }
        try {
            String getter = "is" + Character.toUpperCase(property.charAt(0)) + property.substring(1);
            Method method = obj.getClass().getMethod(getter);
            return method.invoke(obj);
        } catch (Exception ignored) {
        }
        try {
            java.lang.reflect.Field field = findField(obj.getClass(), property);
            if (field != null) {
                field.setAccessible(true);
                return field.get(obj);
            }
        } catch (Exception ignored) {
        }
        LOGGER.warn("Could not resolve property '{}' on {}", property, obj.getClass().getName());
        return null;
    }

    private java.lang.reflect.Field findField(Class<?> clazz, String name) {
        while (clazz != null && clazz != Object.class) {
            try {
                return clazz.getDeclaredField(name);
            } catch (NoSuchFieldException e) {
                clazz = clazz.getSuperclass();
            }
        }
        return null;
    }

    // -------------------------------------------------------------------------
    // Snapshot persistence
    // -------------------------------------------------------------------------

    @SuppressWarnings("unchecked")
    private Map<String, JsonNode> loadSnapshot(DelegateExecution execution, String key) {
        Object raw = execution.getVariable(key);
        if (raw == null) {
            return new HashMap<>();
        }
        try {
            String json = raw instanceof String s ? s : objectMapper.writeValueAsString(raw);
            Map<String, Object> flat = objectMapper.readValue(json,
                    objectMapper.getTypeFactory().constructMapType(HashMap.class, String.class, Object.class));
            Map<String, JsonNode> result = new HashMap<>();
            for (Map.Entry<String, Object> entry : flat.entrySet()) {
                result.put(entry.getKey(), toJsonNode(entry.getValue()));
            }
            return result;
        } catch (Exception e) {
            LOGGER.warn("Could not load variable tracker snapshot for key '{}': {}", key, e.getMessage());
            return new HashMap<>();
        }
    }

    private void saveSnapshot(DelegateExecution execution, String key, Map<String, JsonNode> snapshot) {
        try {
            execution.setVariable(key, objectMapper.writeValueAsString(snapshot));
        } catch (Exception e) {
            LOGGER.error("Could not save variable tracker snapshot for key '{}'", key, e);
        }
    }

    // -------------------------------------------------------------------------
    // Helpers
    // -------------------------------------------------------------------------

    private JsonNode toJsonNode(Object value) {
        if (value == null) {
            return objectMapper.nullNode();
        }
        if (value instanceof JsonNode jn) {
            return jn;
        }
        return objectMapper.valueToTree(value);
    }

    private boolean jsonEquals(JsonNode a, JsonNode b) {
        if (a == null && b == null) return true;
        if (a == null || b == null) return false;
        return a.equals(b);
    }
}
