package click.kamil.springstatemachineexporter.analysis.resolver;

import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.BooleanLiteral;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.ConditionalExpression;
import org.eclipse.jdt.core.dom.EnumConstantDeclaration;
import org.eclipse.jdt.core.dom.EnumDeclaration;
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.ExpressionStatement;
import org.eclipse.jdt.core.dom.FieldAccess;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.ParenthesizedExpression;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.ReturnStatement;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SimpleType;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.SwitchCase;
import org.eclipse.jdt.core.dom.SwitchExpression;
import org.eclipse.jdt.core.dom.SwitchStatement;
import org.eclipse.jdt.core.dom.ThisExpression;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.YieldStatement;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Evaluates no-arg boolean member calls in dispatcher constraints (e.g. {@code eventType.isEvent()})
 * against concrete enum constants by reading enum source — no hard-coded predicate names.
 */
public final class EnumMemberPredicateEvaluator {

    private static final Pattern PREDICATE_PART =
            Pattern.compile("!?\\s*([a-zA-Z][\\w]*)\\s*\\.\\s*([a-zA-Z][\\w]*)\\s*\\(\\s*\\)");

    private EnumMemberPredicateEvaluator() {
    }

    public record PredicateCall(String receiverName, String methodName, boolean negated) {
    }

    public static boolean hasEnumMemberPredicates(String constraint) {
        return !extractPredicateCalls(constraint).isEmpty();
    }

    public static List<PredicateCall> extractPredicateCalls(String constraint) {
        if (constraint == null || constraint.isBlank()) {
            return List.of();
        }
        List<PredicateCall> calls = new ArrayList<>();
        for (String part : ConstraintClauses.splitAndClauses(constraint)) {
            Matcher matcher = PREDICATE_PART.matcher(part.trim());
            if (matcher.matches()) {
                boolean negated = part.trim().startsWith("!");
                calls.add(new PredicateCall(matcher.group(1), matcher.group(2), negated));
            }
        }
        return calls;
    }

    /**
     * Evaluate a single no-arg boolean predicate on one enum constant (method name from dataflow/AST).
     */
    public static Boolean evaluateConstantPredicate(
            String canonicalConstantFqn,
            String enumTypeFqn,
            String methodName,
            boolean negated,
            CodebaseContext context) {
        if (canonicalConstantFqn == null || enumTypeFqn == null || methodName == null || context == null) {
            return null;
        }
        EnumDeclaration enumDecl = findEnumDeclaration(enumTypeFqn, context);
        if (enumDecl == null) {
            return null;
        }
        String constantName = constantSimpleName(canonicalConstantFqn);
        if (constantName == null) {
            return null;
        }
        Map<String, Boolean> boolFields = resolveConstantBooleanFields(enumDecl, constantName);
        Boolean methodResult = evaluateBooleanMethod(
                enumDecl, methodName, constantName, boolFields, context);
        if (methodResult == null) {
            return null;
        }
        return negated ? !methodResult : methodResult;
    }

    public static List<String> filterEnumConstants(
            List<String> candidates,
            String constraint,
            String enumTypeFqn,
            CodebaseContext context) {
        if (candidates == null || candidates.isEmpty()) {
            return candidates == null ? List.of() : candidates;
        }
        List<PredicateCall> predicates = extractPredicateCalls(constraint);
        if (predicates.isEmpty()) {
            return candidates;
        }
        EnumDeclaration enumDecl = findEnumDeclaration(enumTypeFqn, context);
        if (enumDecl == null) {
            return List.of();
        }
        List<String> filtered = new ArrayList<>();
        for (String candidate : candidates) {
            if (satisfiesPredicates(candidate, predicates, enumDecl, context)) {
                filtered.add(candidate);
            }
        }
        return filtered;
    }

    /**
     * True when every predicate method in the constraint exists on the enum (or its interfaces).
     * Used to distinguish inconclusive filtering from an intentional empty result.
     */
    public static boolean hasResolvablePredicateMethods(
            String constraint,
            String enumTypeFqn,
            CodebaseContext context) {
        List<PredicateCall> predicates = extractPredicateCalls(constraint);
        if (predicates.isEmpty()) {
            return true;
        }
        EnumDeclaration enumDecl = findEnumDeclaration(enumTypeFqn, context);
        if (enumDecl == null) {
            return false;
        }
        for (PredicateCall predicate : predicates) {
            MethodDeclaration method = findParameterlessMethod(enumDecl, predicate.methodName());
            if (method == null && context != null) {
                method = findInterfaceParameterlessMethod(enumDecl, predicate.methodName(), context);
            }
            if (method == null) {
                return false;
            }
        }
        return true;
    }

    private static boolean satisfiesPredicates(
            String canonicalConstantFqn,
            List<PredicateCall> predicates,
            EnumDeclaration enumDecl,
            CodebaseContext context) {
        String constantName = constantSimpleName(canonicalConstantFqn);
        if (constantName == null) {
            return false;
        }
        Map<String, Boolean> boolFields = resolveConstantBooleanFields(enumDecl, constantName);
        for (PredicateCall predicate : predicates) {
            Boolean methodResult = evaluateBooleanMethod(
                    enumDecl, predicate.methodName(), constantName, boolFields, context);
            if (methodResult == null) {
                return false;
            }
            boolean expected = predicate.negated ? !methodResult : methodResult;
            if (!expected) {
                return false;
            }
        }
        return true;
    }

    static Boolean evaluateBooleanMethod(
            EnumDeclaration enumDecl,
            String methodName,
            String currentConstantName,
            Map<String, Boolean> boolFields,
            CodebaseContext context) {
        MethodDeclaration method = findParameterlessMethod(enumDecl, methodName);
        if (method == null && context != null) {
            method = findInterfaceParameterlessMethod(enumDecl, methodName, context);
        }
        if (method == null || method.getBody() == null) {
            return null;
        }
        Boolean switchResult = evaluateSwitchDrivenMethod(
                method, currentConstantName, boolFields, enumDecl, context);
        if (switchResult != null) {
            return switchResult;
        }
        Boolean[] result = new Boolean[1];
        method.getBody().accept(new ASTVisitor() {
            @Override
            public boolean visit(ReturnStatement node) {
                if (result[0] == null) {
                    result[0] = evaluateBooleanExpression(
                            node.getExpression(), currentConstantName, boolFields, enumDecl, context);
                }
                return super.visit(node);
            }
        });
        return result[0];
    }

    private static Boolean evaluateSwitchDrivenMethod(
            MethodDeclaration method,
            String currentConstantName,
            Map<String, Boolean> boolFields,
            EnumDeclaration enumDecl,
            CodebaseContext context) {
        Boolean[] result = new Boolean[1];
        method.getBody().accept(new ASTVisitor() {
            @Override
            public boolean visit(SwitchStatement node) {
                if (result[0] == null && isEnumSelfSelector(node.getExpression())) {
                    result[0] = evaluateSwitchLike(
                            node.statements(), currentConstantName, boolFields, enumDecl, context);
                }
                return result[0] == null;
            }

            @Override
            public boolean visit(SwitchExpression node) {
                if (result[0] == null && isEnumSelfSelector(node.getExpression())) {
                    result[0] = evaluateSwitchLike(
                            node.statements(), currentConstantName, boolFields, enumDecl, context);
                }
                return result[0] == null;
            }
        });
        return result[0];
    }

    static Map<String, Boolean> resolveConstantBooleanFields(EnumDeclaration enumDecl, String constantName) {
        Map<String, Boolean> fields = new HashMap<>();
        EnumConstantDeclaration constant = findEnumConstant(enumDecl, constantName);
        if (constant == null) {
            return fields;
        }
        MethodDeclaration constructor = findEnumConstructor(enumDecl);
        if (constructor != null && constant.arguments().size() == constructor.parameters().size()) {
            for (int i = 0; i < constructor.parameters().size(); i++) {
                SingleVariableDeclaration param =
                        (SingleVariableDeclaration) constructor.parameters().get(i);
                Expression arg = (Expression) constant.arguments().get(i);
                Boolean boolVal = literalBoolean(arg);
                if (boolVal != null) {
                    fields.put(param.getName().getIdentifier(), boolVal);
                    mapFieldName(enumDecl, param.getName().getIdentifier(), boolVal, fields);
                }
            }
        }
        for (Object bodyObj : enumDecl.bodyDeclarations()) {
            if (bodyObj instanceof FieldDeclaration fd) {
                for (Object fragObj : fd.fragments()) {
                    if (fragObj instanceof VariableDeclarationFragment fragment) {
                        Expression init = fragment.getInitializer();
                        Boolean boolVal = literalBoolean(init);
                        if (boolVal != null) {
                            fields.put(fragment.getName().getIdentifier(), boolVal);
                        }
                    }
                }
            }
        }
        return fields;
    }

    private static void mapFieldName(
            EnumDeclaration enumDecl,
            String paramName,
            Boolean value,
            Map<String, Boolean> fields) {
        for (Object bodyObj : enumDecl.bodyDeclarations()) {
            if (!(bodyObj instanceof FieldDeclaration fd)) {
                continue;
            }
            for (Object fragObj : fd.fragments()) {
                if (fragObj instanceof VariableDeclarationFragment fragment
                        && fragment.getName().getIdentifier().equals(paramName)) {
                    fields.put(paramName, value);
                }
            }
        }
    }

    private static Boolean evaluateBooleanExpression(
            Expression expr,
            String currentConstantName,
            Map<String, Boolean> boolFields,
            EnumDeclaration enumDecl,
            CodebaseContext context) {
        if (expr instanceof BooleanLiteral bl) {
            return bl.booleanValue();
        }
        if (expr instanceof SimpleName sn) {
            return boolFields.get(sn.getIdentifier());
        }
        if (expr instanceof FieldAccess fa) {
            if (fa.getExpression() == null || fa.getExpression() instanceof ThisExpression) {
                return boolFields.get(fa.getName().getIdentifier());
            }
        }
        if (expr instanceof ParenthesizedExpression pe) {
            return evaluateBooleanExpression(
                    pe.getExpression(), currentConstantName, boolFields, enumDecl, context);
        }
        if (expr instanceof ConditionalExpression ce) {
            Boolean thenVal = evaluateBooleanExpression(
                    ce.getThenExpression(), currentConstantName, boolFields, enumDecl, context);
            Boolean elseVal = evaluateBooleanExpression(
                    ce.getElseExpression(), currentConstantName, boolFields, enumDecl, context);
            if (thenVal != null && elseVal != null && thenVal.equals(elseVal)) {
                return thenVal;
            }
            return null;
        }
        if (expr instanceof SwitchExpression se && isEnumSelfSelector(se.getExpression())) {
            return evaluateSwitchLike(se.statements(), currentConstantName, boolFields, enumDecl, context);
        }
        if (expr instanceof MethodInvocation mi
                && mi.arguments().isEmpty()
                && enumDecl != null
                && context != null) {
            return evaluateBooleanMethod(
                    enumDecl, mi.getName().getIdentifier(), currentConstantName, boolFields, context);
        }
        return null;
    }

    private static Boolean evaluateSwitchLike(
            List<?> statements,
            String currentConstantName,
            Map<String, Boolean> boolFields,
            EnumDeclaration enumDecl,
            CodebaseContext context) {
        List<String> caseConstants = new ArrayList<>();
        boolean defaultCase = false;
        boolean sawArmBody = false;
        boolean pendingCase = false;
        for (Object statementObj : statements) {
            if (statementObj instanceof SwitchCase switchCase) {
                if (sawArmBody) {
                    caseConstants.clear();
                    defaultCase = false;
                    sawArmBody = false;
                }
                pendingCase = true;
                if (switchCase.isDefault()) {
                    defaultCase = true;
                } else {
                    for (Object exprObj : switchCase.expressions()) {
                        if (exprObj instanceof Expression caseExpr) {
                            String caseConstant = extractCaseConstantName(caseExpr);
                            if (caseConstant != null) {
                                caseConstants.add(caseConstant);
                            }
                        }
                    }
                }
                continue;
            }
            if (!pendingCase) {
                continue;
            }
            sawArmBody = true;
            if (!defaultCase && !caseConstants.contains(currentConstantName)) {
                continue;
            }
            Boolean armValue = evaluateSwitchArmNode(
                    statementObj, currentConstantName, boolFields, enumDecl, context);
            if (armValue != null) {
                return armValue;
            }
        }
        return null;
    }

    private static Boolean evaluateSwitchArmNode(
            Object node,
            String currentConstantName,
            Map<String, Boolean> boolFields,
            EnumDeclaration enumDecl,
            CodebaseContext context) {
        if (node instanceof ReturnStatement rs) {
            return evaluateBooleanExpression(
                    rs.getExpression(), currentConstantName, boolFields, enumDecl, context);
        }
        if (node instanceof YieldStatement ys) {
            return evaluateBooleanExpression(
                    ys.getExpression(), currentConstantName, boolFields, enumDecl, context);
        }
        if (node instanceof ExpressionStatement es) {
            return evaluateBooleanExpression(
                    es.getExpression(), currentConstantName, boolFields, enumDecl, context);
        }
        if (node instanceof Block block) {
            return evaluateLinearBooleanStatements(
                    block.statements(), currentConstantName, boolFields, enumDecl, context);
        }
        if (node instanceof Expression expression) {
            return evaluateBooleanExpression(
                    expression, currentConstantName, boolFields, enumDecl, context);
        }
        return null;
    }

    private static Boolean evaluateLinearBooleanStatements(
            List<?> statements,
            String currentConstantName,
            Map<String, Boolean> boolFields,
            EnumDeclaration enumDecl,
            CodebaseContext context) {
        for (Object statementObj : statements) {
            Boolean value = evaluateSwitchArmNode(
                    statementObj, currentConstantName, boolFields, enumDecl, context);
            if (value != null) {
                return value;
            }
        }
        return null;
    }

    private static boolean isEnumSelfSelector(Expression expr) {
        if (expr instanceof ThisExpression) {
            return true;
        }
        if (expr instanceof ParenthesizedExpression pe) {
            return isEnumSelfSelector(pe.getExpression());
        }
        return false;
    }

    private static String extractCaseConstantName(Expression expr) {
        if (expr instanceof SimpleName simpleName) {
            return simpleName.getIdentifier();
        }
        if (expr instanceof QualifiedName qualifiedName) {
            return qualifiedName.getName().getIdentifier();
        }
        if (expr instanceof FieldAccess fieldAccess) {
            return fieldAccess.getName().getIdentifier();
        }
        if (expr instanceof ParenthesizedExpression pe) {
            return extractCaseConstantName(pe.getExpression());
        }
        return null;
    }

    private static Boolean literalBoolean(Expression expr) {
        return expr instanceof BooleanLiteral bl ? bl.booleanValue() : null;
    }

    private static MethodDeclaration findParameterlessMethod(AbstractTypeDeclaration typeDecl, String methodName) {
        if (typeDecl == null) {
            return null;
        }
        for (Object bodyObj : typeDecl.bodyDeclarations()) {
            if (bodyObj instanceof MethodDeclaration md
                    && !md.isConstructor()
                    && methodName.equals(md.getName().getIdentifier())
                    && md.parameters().isEmpty()) {
                return md;
            }
        }
        return null;
    }

    private static MethodDeclaration findInterfaceParameterlessMethod(
            EnumDeclaration enumDecl,
            String methodName,
            CodebaseContext context) {
        CompilationUnit cu = enumDecl.getRoot() instanceof CompilationUnit root ? root : null;
        for (Object typeObj : enumDecl.superInterfaceTypes()) {
            if (!(typeObj instanceof SimpleType simpleType)) {
                continue;
            }
            String interfaceName = simpleType.getName().getFullyQualifiedName();
            TypeDeclaration interfaceDecl = context.getTypeDeclaration(interfaceName, cu);
            MethodDeclaration method = findParameterlessMethod(interfaceDecl, methodName);
            if (method != null) {
                return method;
            }
        }
        return null;
    }

    private static MethodDeclaration findEnumConstructor(EnumDeclaration enumDecl) {
        MethodDeclaration implicit = null;
        for (Object bodyObj : enumDecl.bodyDeclarations()) {
            if (bodyObj instanceof MethodDeclaration md && md.isConstructor()) {
                if (md.parameters().isEmpty()) {
                    implicit = md;
                } else {
                    return md;
                }
            }
        }
        return implicit;
    }

    private static EnumConstantDeclaration findEnumConstant(EnumDeclaration enumDecl, String constantName) {
        for (Object constantObj : enumDecl.enumConstants()) {
            if (constantObj instanceof EnumConstantDeclaration ecd
                    && constantName.equals(ecd.getName().getIdentifier())) {
                return ecd;
            }
        }
        return null;
    }

    static EnumDeclaration findEnumDeclaration(String enumTypeFqn, CodebaseContext context) {
        if (enumTypeFqn == null || context == null) {
            return null;
        }
        String clean = enumTypeFqn.contains("<") ? enumTypeFqn.substring(0, enumTypeFqn.indexOf('<')) : enumTypeFqn;
        for (CompilationUnit cu : context.getCompilationUnits()) {
            String pkg = cu.getPackage() != null ? cu.getPackage().getName().getFullyQualifiedName() : "";
            for (Object typeObj : cu.types()) {
                EnumDeclaration found = findEnumNode(typeObj, clean, pkg, context);
                if (found != null) {
                    return found;
                }
            }
        }
        return null;
    }

    private static EnumDeclaration findEnumNode(Object typeObj, String targetFqn, String pkg, CodebaseContext context) {
        if (typeObj instanceof EnumDeclaration ed) {
            String fqn = pkg.isEmpty() ? ed.getName().getIdentifier() : pkg + "." + ed.getName().getIdentifier();
            if (targetFqn.equals(fqn)) {
                return ed;
            }
        } else if (typeObj instanceof TypeDeclaration td) {
            String parentFqn = pkg.isEmpty() ? td.getName().getIdentifier() : pkg + "." + td.getName().getIdentifier();
            for (Object decl : td.bodyDeclarations()) {
                if (decl instanceof EnumDeclaration nested) {
                    String nestedFqn = parentFqn + "." + nested.getName().getIdentifier();
                    if (targetFqn.equals(nestedFqn)) {
                        return nested;
                    }
                }
            }
        }
        return null;
    }

    private static String constantSimpleName(String canonicalConstantFqn) {
        if (canonicalConstantFqn == null || !canonicalConstantFqn.contains(".")) {
            return canonicalConstantFqn;
        }
        return canonicalConstantFqn.substring(canonicalConstantFqn.lastIndexOf('.') + 1);
    }
}
