package com.customer.work.service;

import com.fasterxml.jackson.databind.JsonNode;
import org.flowable.common.engine.api.variable.VariableContainer;
import org.flowable.common.engine.impl.el.function.AbstractFlowableVariableExpressionFunction;
import org.springframework.stereotype.Component;

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

/**
 * Flowable EL function delegate that reads a variable by dot-separated path.
 * Works in both BPMN processes and CMMN cases without any explicit scope parameter.
 *
 * Flowable's AbstractFlowableVariableExpressionFunction automatically injects the
 * current VariableContainer (execution / planItemInstance / caseInstance) as the
 * first argument before the expression is evaluated, so no ThreadLocal or listener
 * is required.
 *
 * Registered automatically by Flowable's Spring Boot integration when it finds a
 * FlowableFunctionDelegate bean in the application context.
 *
 * Usage in a Flowable backend expression (prefixes var / vars / variables all work):
 *   ${var:getPath('myVar')}
 *   ${var:getPath('order.customer.name')}
 *   ${var:getPath('order.lines.0.price')}   ← list index by position
 *
 * Path resolution per segment:
 *   - java.util.List / array  → integer index  (e.g. "2")
 *   - java.util.Map           → key lookup
 *   - Jackson JsonNode        → field lookup, scalar nodes unwrapped to Java types
 *   - POJO                    → public getter (getX / isX) or field, superclass included
 *
 * Returns null when any segment along the path does not exist.
 */
@Component
public class VariableGetPathFunction extends AbstractFlowableVariableExpressionFunction {

    public VariableGetPathFunction() {
        super(List.of("getPath"), "getPath");
    }

    /**
     * Called by Flowable's EL engine.  The {@code container} is injected automatically;
     * {@code path} is the dot-separated path string supplied in the expression.
     */
    public static Object getPath(VariableContainer container, String path) {
        if (container == null || path == null || path.isBlank()) {
            return null;
        }

        String[] segments = path.split("\\.", -1);
        Object current = container.getVariable(segments[0]);

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

        return current;
    }

    // -------------------------------------------------------------------------
    // Path navigation
    // -------------------------------------------------------------------------

    @SuppressWarnings("unchecked")
    private static Object step(Object obj, String segment) {

        // List / array index
        if (obj instanceof List<?> list) {
            try {
                int idx = Integer.parseInt(segment);
                return (idx >= 0 && idx < list.size()) ? list.get(idx) : null;
            } catch (NumberFormatException ignored) {
            }
        }
        if (obj instanceof Object[] arr) {
            try {
                int idx = Integer.parseInt(segment);
                return (idx >= 0 && idx < arr.length) ? arr[idx] : null;
            } catch (NumberFormatException ignored) {
            }
        }

        // Map key lookup
        if (obj instanceof Map<?, ?> map) {
            return map.get(segment);
        }

        // Jackson JsonNode — unwrap scalar nodes to plain Java values
        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 with superclass walk
        try {
            Method m = obj.getClass().getMethod(
                    "get" + Character.toUpperCase(segment.charAt(0)) + segment.substring(1));
            return m.invoke(obj);
        } catch (Exception ignored) {
        }
        try {
            Method m = obj.getClass().getMethod(
                    "is" + Character.toUpperCase(segment.charAt(0)) + segment.substring(1));
            return m.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) {
        }

        return null;
    }

    private static 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;
    }
}
