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.cmmn.engine.CmmnEngineConfiguration;
import org.flowable.common.engine.api.delegate.event.FlowableEntityEvent;
import org.flowable.common.engine.api.delegate.event.FlowableEvent;
import org.flowable.common.engine.api.delegate.event.FlowableEventListener;
import org.flowable.common.engine.api.variable.VariableContainer;
import org.flowable.engine.ProcessEngineConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * General-purpose Flowable variable utility bean.
 *
 * Usable in BPMN process and CMMN case backend expressions without any
 * scope parameter — the current execution / plan-item instance is captured
 * automatically via a Flowable event listener registered on both engines.
 *
 * Expression examples:
 *   ${varUtils.get('order.customer.name')}
 *   ${varUtils.track('order.customer.name,order.total,status')}
 *
 * How the scope is resolved without a parameter
 * -----------------------------------------------
 * Flowable fires ACTIVITY_STARTED (BPMN) or PLAN_ITEM_INSTANCE_STARTED (CMMN)
 * on the same thread — and strictly before — the service-task expression is
 * evaluated.  onEvent() stores the current VariableContainer in a ThreadLocal.
 * The ThreadLocal is cleared when the activity / plan item completes or is
 * cancelled, so it never leaks across tasks.
 *
 * Registration
 * ------------
 * SmartInitializingSingleton.afterSingletonsInstantiated() runs after all
 * Spring beans (including engine configurations) are fully initialised, so
 * addEventListener() is always called on a live event dispatcher.
 * Only the engine configurations that are present in the application context
 * are registered; the other one is silently skipped.
 */
@Component("varUtils")
public class VarUtils implements FlowableEventListener, SmartInitializingSingleton {

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

    private static final ThreadLocal<VariableContainer> SCOPE = new ThreadLocal<>();

    private static final ObjectMapper MAPPER;
    static {
        MAPPER = new ObjectMapper();
        MAPPER.registerModule(new JavaTimeModule());
        MAPPER.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
    }

    @Autowired(required = false)
    private ProcessEngineConfiguration processEngineConfiguration;

    @Autowired(required = false)
    private CmmnEngineConfiguration cmmnEngineConfiguration;

    // -------------------------------------------------------------------------
    // Listener registration
    // -------------------------------------------------------------------------

    @Override
    public void afterSingletonsInstantiated() {
        if (processEngineConfiguration != null) {
            processEngineConfiguration.getEventDispatcher().addEventListener(this);
            LOGGER.debug("varUtils registered on BPMN event dispatcher");
        }
        if (cmmnEngineConfiguration != null) {
            cmmnEngineConfiguration.getEventDispatcher().addEventListener(this);
            LOGGER.debug("varUtils registered on CMMN event dispatcher");
        }
    }

    // -------------------------------------------------------------------------
    // FlowableEventListener — bind / unbind the scope ThreadLocal
    // -------------------------------------------------------------------------

    @Override
    public void onEvent(FlowableEvent event) {
        if (!(event instanceof FlowableEntityEvent entityEvent)) return;
        Object entity = entityEvent.getEntity();
        if (!(entity instanceof VariableContainer vc)) return;

        String eventName = event.getType().name();
        LOGGER.info("eventName={}", eventName);

        switch (eventName) {
            case "CASE_STARTED",
                 "PROCESS_STARTED",
                 "ACTIVITY_STARTED",
                 "PLAN_ITEM_INSTANCE_STARTED"     -> SCOPE.set(vc);
            case "CASE_ENDED",
                 "PROCESS_COMPLETED",
                 "ACTIVITY_COMPLETED",
                 "ACTIVITY_CANCELLED",
                 "PLAN_ITEM_INSTANCE_COMPLETED",
                 "PLAN_ITEM_INSTANCE_TERMINATED",
                 "PLAN_ITEM_INSTANCE_SUSPENDED"   -> SCOPE.remove();
            default -> { /* ignore */ }
        }
    }

    @Override public boolean isFailOnException()              { return false; }
    @Override public boolean isFireOnTransactionLifecycleEvent() { return false; }
    @Override public String  getOnTransaction()               { return null; }

    // -------------------------------------------------------------------------
    // Public API — called from Flowable expressions
    // -------------------------------------------------------------------------

    /**
     * Returns the value at the given dot-separated path within the current
     * variable scope.  Supports Maps, Lists (integer index), Jackson JsonNodes,
     * and POJOs (getter or field).  Returns null for any missing segment.
     *
     *   ${varUtils.get('order.customer.name')}
     *   ${varUtils.get('order.lines.0.price')}
     */
    public Object get(String path) {
        VariableContainer scope = currentScope("get");
        if (scope == null || path == null || path.isBlank()) return null;
        return resolvePath(scope, path.trim());
    }

    /**
     * Checks which of the given comma-separated variable paths changed since the
     * last call and returns a JSON array describing each change.
     *
     *   ${varUtils.track('order.customer.name,order.total,status')}
     *
     * Returns [] on the first call (baseline snapshot recorded).
     * Each subsequent call compares against the previous snapshot and updates it.
     * The snapshot is stored inside the variable scope so it survives across tasks.
     *
     * Example return value:
     *   [{"path":"order.total","oldValue":100,"newValue":150}]
     */
    public String track(String pathsCsv) {
        VariableContainer scope = currentScope("track");
        if (scope == null || pathsCsv == null || pathsCsv.isBlank()) return "[]";

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

        String snapshotKey = SNAPSHOT_PREFIX + pathsCsv.replaceAll("\\s+", "");
        Map<String, JsonNode> previous = loadSnapshot(scope, snapshotKey);
        Map<String, JsonNode> current  = new HashMap<>();
        ArrayNode changes = MAPPER.createArrayNode();

        for (String path : paths) {
            JsonNode value = toJson(resolvePath(scope, path));
            current.put(path, value);

            if (previous.isEmpty()) continue; // first call — baseline only

            JsonNode prev = previous.getOrDefault(path, MAPPER.nullNode());
            if (!prev.equals(value)) {
                ObjectNode change = MAPPER.createObjectNode();
                change.put("path", path);
                change.set("oldValue", prev);
                change.set("newValue", value);
                changes.add(change);
            }
        }

        saveSnapshot(scope, snapshotKey, current);

        try {
            return MAPPER.writeValueAsString(changes);
        } catch (Exception e) {
            LOGGER.error("varUtils.track: failed to serialize changes", e);
            return "[]";
        }
    }

    // -------------------------------------------------------------------------
    // Path resolution
    // -------------------------------------------------------------------------

    private static Object resolvePath(VariableContainer scope, String path) {
        String[] segments = path.split("\\.", -1);
        Object current = scope.getVariable(segments[0]);
        for (int i = 1; i < segments.length; i++) {
            if (current == null) return null;
            current = step(current, segments[i]);
        }
        return current;
    }

    @SuppressWarnings("unchecked")
    private static Object step(Object obj, String segment) {
        if (obj instanceof List<?> list) {
            try {
                int i = Integer.parseInt(segment);
                return i >= 0 && i < list.size() ? list.get(i) : null;
            } catch (NumberFormatException ignored) {}
        }
        if (obj instanceof Object[] arr) {
            try {
                int i = Integer.parseInt(segment);
                return i >= 0 && i < arr.length ? arr[i] : null;
            } catch (NumberFormatException ignored) {}
        }
        if (obj instanceof Map<?, ?> map) return map.get(segment);
        if (obj instanceof JsonNode jn) {
            JsonNode node = jn.get(segment);
            if (node == null || node.isNull()) return null;
            if (node.isTextual()) return node.asText();
            if (node.isBoolean()) return node.asBoolean();
            if (node.isLong())    return node.asLong();
            if (node.isInt())     return node.asInt();
            if (node.isDouble())  return node.asDouble();
            return node;
        }
        // POJO: try getter (getX / isX) then field
        String cap = Character.toUpperCase(segment.charAt(0)) + segment.substring(1);
        try { return obj.getClass().getMethod("get" + cap).invoke(obj); } catch (Exception ignored) {}
        try { return obj.getClass().getMethod("is"  + cap).invoke(obj); } catch (Exception ignored) {}
        try {
            java.lang.reflect.Field f = findField(obj.getClass(), segment);
            if (f != null) { f.setAccessible(true); return f.get(obj); }
        } catch (Exception ignored) {}
        LOGGER.warn("varUtils: cannot resolve '{}' on {}", segment, obj.getClass().getName());
        return null;
    }

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

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

    private static Map<String, JsonNode> loadSnapshot(VariableContainer scope, String key) {
        Object raw = scope.getVariable(key);
        if (raw == null) return new HashMap<>();
        try {
            String json = raw instanceof String s ? s : MAPPER.writeValueAsString(raw);
            Map<String, Object> flat = MAPPER.readValue(json,
                    MAPPER.getTypeFactory().constructMapType(HashMap.class, String.class, Object.class));
            Map<String, JsonNode> result = new HashMap<>();
            flat.forEach((k, v) -> result.put(k, toJson(v)));
            return result;
        } catch (Exception e) {
            LOGGER.warn("varUtils: could not load snapshot '{}': {}", key, e.getMessage());
            return new HashMap<>();
        }
    }

    private static void saveSnapshot(VariableContainer scope, String key, Map<String, JsonNode> snapshot) {
        try {
            scope.setVariable(key, MAPPER.writeValueAsString(snapshot));
        } catch (Exception e) {
            LOGGER.error("varUtils: could not save snapshot '{}'", key, e);
        }
    }

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

    private VariableContainer currentScope(String method) {
        VariableContainer scope = SCOPE.get();
        if (scope == null) {
            LOGGER.error("varUtils.{}() called without an active Flowable scope — "
                    + "is VarUtils registered on the engine event dispatcher?", method);
        }
        return scope;
    }

    private static JsonNode toJson(Object value) {
        if (value == null) return MAPPER.nullNode();
        if (value instanceof JsonNode jn) return jn;
        return MAPPER.valueToTree(value);
    }
}
