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.api.CmmnRuntimeService;
import org.flowable.cmmn.engine.CmmnEngineConfiguration;
import org.flowable.cmmn.engine.impl.persistence.entity.CaseInstanceEntity;
import org.flowable.common.engine.api.delegate.event.FlowableEngineEntityEvent;
import org.flowable.common.engine.api.delegate.event.FlowableEvent;
import org.flowable.common.engine.api.delegate.event.FlowableEventListener;
import org.flowable.engine.ProcessEngineConfiguration;
import org.flowable.cmmn.api.runtime.CaseInstance;
import org.flowable.cmmn.api.runtime.PlanItemInstance;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.impl.persistence.entity.ExecutionEntity;
import org.flowable.engine.runtime.Execution;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.variable.api.delegate.VariableScope;
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:
 *   ${varUtils.get('order.customer.name')}
 *   ${varUtils.track('order.customer.name,order.total,status')}
 * Root variables are supported.
 * How the scope is resolved without a parameter
 * -----------------------------------------------
 * The bean registers itself as a Flowable event listener on both engines.
 * Before a service task expression is evaluated, Flowable fires events on the
 * same thread that allow us to capture the current variable scope:
 *   BPMN  — PROCESS_STARTED fires a FlowableEngineEntityEvent.
 *           The execution is retrieved via event.getEntity().

 *   CMMN  — CASE_STARTED fires a FlowableEngineEntityEvent.;  Instead, when a plan
 *           The case instance is retrieved via event.getEntity().
 */
@Component("varUtils")
public class VarUtils implements FlowableEventListener, SmartInitializingSingleton {

    private static final Logger LOGGER = LoggerFactory.getLogger(VarUtils.class);

    private static final ThreadLocal<VariableScope> 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
    private ProcessEngineConfiguration processEngineConfiguration;

    @Autowired
    private CmmnEngineConfiguration cmmnEngineConfiguration;

    @Autowired
    private RuntimeService runtimeService;

    @Autowired
    private CmmnRuntimeService cmmnRuntimeService;

    // -------------------------------------------------------------------------
    // Listener registration — runs after all beans are ready
    // -------------------------------------------------------------------------
    @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 FlowableEngineEntityEvent entityEvent) {
            String typeName = event.getType().name();
            Object entity = entityEvent.getEntity();

            // BPMN execution
            if (entity instanceof ExecutionEntity execution) {
                switch (typeName) {
                    case "PROCESS_STARTED"
                            -> SCOPE.set(execution);
                    case "PROCESS_CANCELLED",
                         "PROCESS_COMPLETED",
                         "PROCESS_COMPLETED_WITH_ERROR_END_EVENT",
                         "PROCESS_COMPLETED_WITH_ESCALATION_END_EVENT",
                         "PROCESS_COMPLETED_WITH_TERMINATE_END_EVENT"
                            -> SCOPE.remove();
                }
                return;
            }

            // CMMN case instance itself
            if (entity instanceof CaseInstanceEntity caseInstance) {
                if ("CASE_STARTED".equals(typeName))
                    SCOPE.set(caseInstance);
                else if ("CASE_ENDED".equals(typeName))
                    SCOPE.remove();
            }
        }
    }

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

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

    /**
     * Checks which of the given comma-separated variable paths changed since the
     * last call and returns a JSON array describing each change.
     *   ${varUtils.trackVars('root.snapshot', 'root.myString, status')}
     * {@code snapshotPath} is a dot-notation path (supports the {@code root.} prefix)
     * pointing to where the previous-values snapshot is stored and updated.
     * Each subsequent call compares against the previous snapshot and updates it.
     * Example return value:
     *   [{"path":"root.myString","oldValue":"a","newValue":"b"}]
     */
    public ArrayNode trackVars(String snapshotPath, String variablePathsCsv) {

        // Check parameters
        if (snapshotPath == null || snapshotPath.isBlank() || variablePathsCsv == null || variablePathsCsv.isBlank()) {
            LOGGER.debug("{}.trackVars: empty parameters", getClass().getName());
            return MAPPER.createArrayNode();
        }

        // Get current scope
        VariableScope currentScope = getCurrentScope();
        if (currentScope == null) {
            LOGGER.debug("{}.trackVars: currentScope not found", getClass().getName());
            return MAPPER.createArrayNode();
        }

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

        Map<String, JsonNode> oldSnapshot = loadSnapshot(currentScope, snapshotPath);
        Map<String, JsonNode> newSnapshot = new HashMap<>();
        ArrayNode changes = MAPPER.createArrayNode();

        // Loop paths list
        for (String path : paths) {
            JsonNode newValue = toJson(readVariableFromPath(currentScope, path));
            newSnapshot.put(path, newValue);

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

        saveSnapshot(currentScope, snapshotPath, newSnapshot);

        return changes;
    }

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

    private Object readVariableFromPath(VariableScope scope, String path) {
        String[] segments = path.split("\\.", -1);
        if (segments.length < 1) return null;

        int startIndex = 0;
        if ("root".equals(segments[0])) {
            if (segments.length < 2) return null;
            startIndex = 1;
            scope = getRootScope(scope);
        }
        if (scope == null) return null;

        Object currentValue = scope.getVariable(segments[startIndex]);
        for (int i = startIndex + 1; i < segments.length; i++) {
            if (currentValue == null) return null;
            currentValue = getNestedVariable(currentValue, segments[i]);
        }
        return currentValue;
    }

    private void writeVariableToPath(VariableScope scope, String path, Object value) {
        String[] segments = path.split("\\.", -1);
        if (segments.length < 1) return;

        VariableScope targetScope;
        String[] varSegments;
        if ("root".equals(segments[0])) {
            if (segments.length < 2) return;
            targetScope = getRootScope(scope);
            varSegments = Arrays.copyOfRange(segments, 1, segments.length);
        } else {
            targetScope = scope;
            varSegments = segments;
        }
        if (targetScope == null) return;

        if (varSegments.length == 1) {
            targetScope.setVariable(varSegments[0], value);
            return;
        }

        // Nested path: read the top-level variable, navigate to the parent node,
        // mutate it in-place, then write the top-level variable back.
        String topVar = varSegments[0];
        Object topValue = targetScope.getVariable(topVar);

        Object parent = topValue;
        for (int i = 1; i < varSegments.length - 1; i++) {
            if (parent == null) {
                LOGGER.warn("varUtils.writeVariableToPath: null at '{}' in path '{}'", varSegments[i - 1], path);
                return;
            }
            parent = getNestedVariable(parent, varSegments[i]);
        }

        if (!setNestedValue(parent, varSegments[varSegments.length - 1], value, path)) return;
        targetScope.setVariable(topVar, topValue);
    }

    @SuppressWarnings("unchecked")
    private boolean setNestedValue(Object parent, String key, Object value, String path) {
        if (parent instanceof Map map) {
            map.put(key, value);
            return true;
        }
        if (parent instanceof ObjectNode on) {
            on.set(key, toJson(value));
            return true;
        }
        LOGGER.warn("varUtils.writeVariableToPath: cannot set '{}' on {} in path '{}'",
                key, parent == null ? "null" : parent.getClass().getName(), path);
        return false;
    }

    // -------------------------------------------------------------------------
    // Scope resolution
    // -------------------------------------------------------------------------

    private VariableScope getCurrentScope() {
        VariableScope scope = SCOPE.get();
        if (scope == null) {
            LOGGER.error("{}.getCurrentScope called without an active Flowable scope", getClass().getName());
        }
        return scope;
    }

    private VariableScope getRootScope(VariableScope scope) {
        if (scope instanceof DelegateExecution ex) return findBpmnRootScope(ex.getProcessInstanceId());
        if (scope instanceof CaseInstance ci) return findCmmnRootScope(ci.getId());
        return null;
    }

    private VariableScope findBpmnRootScope(String processInstanceId) {
        if (processInstanceId == null || runtimeService == null) return null;
        try {
            Execution piExec = runtimeService.createExecutionQuery()
                    .executionId(processInstanceId).singleResult();
            if (piExec != null && piExec.getSuperExecutionId() != null) {
                Execution superExec = runtimeService.createExecutionQuery()
                        .executionId(piExec.getSuperExecutionId()).singleResult();
                if (superExec != null)
                    return findBpmnRootScope(superExec.getProcessInstanceId());
            }
            ProcessInstance pi = runtimeService.createProcessInstanceQuery()
                    .processInstanceId(processInstanceId).singleResult();
            if (pi != null && pi.getCallbackType() != null && pi.getCallbackId() != null
                    && cmmnRuntimeService != null) {
                PlanItemInstance planItem = cmmnRuntimeService.createPlanItemInstanceQuery()
                        .planItemInstanceId(pi.getCallbackId()).singleResult();
                if (planItem != null)
                    return findCmmnRootScope(planItem.getCaseInstanceId());
            }
            return (ExecutionEntity) piExec;  // root: ExecutionEntity implements VariableScope
        } catch (Exception e) {
            LOGGER.debug("{}.findBpmnRootScope: BPMN root scope climb failed: {}", getClass().getName(), e.getMessage());
            return null;
        }
    }

    private VariableScope findCmmnRootScope(String caseInstanceId) {
        if (caseInstanceId == null || cmmnRuntimeService == null) return null;
        try {
            CaseInstance ci = cmmnRuntimeService.createCaseInstanceQuery()
                    .caseInstanceId(caseInstanceId).singleResult();
            if (ci == null) return null;
            if (ci.getParentId() != null)
                return findCmmnRootScope(ci.getParentId());
            if (ci.getCallbackType() != null && ci.getCallbackId() != null
                    && runtimeService != null) {
                Execution callbackExec = runtimeService.createExecutionQuery()
                        .executionId(ci.getCallbackId()).singleResult();
                if (callbackExec != null)
                    return findBpmnRootScope(callbackExec.getProcessInstanceId());
            }
            return (CaseInstanceEntity) ci;  // root: CaseInstanceEntity implements VariableScope
        } catch (Exception e) {
            LOGGER.debug("{}.findCmmnRootScope: CMMN root scope climb failed: {}", getClass().getName(), e.getMessage());
            return null;
        }
    }

    private static Object getNestedVariable(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;
        }
        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 Map<String, JsonNode> loadSnapshot(VariableScope scope, String path) {
        Object raw = readVariableFromPath(scope, path);
        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 at '{}': {}", path, e.getMessage());
            return new HashMap<>();
        }
    }

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

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

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