package click.kamil.springstatemachineexporter.analysis.resolver;

import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.analysis.service.TypeResolver;
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.ClassInstanceCreation;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.InfixExpression;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.ReturnStatement;
import org.eclipse.jdt.core.dom.TypeDeclaration;

import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Evaluates boolean predicates on rich/domain event types (e.g. {@code myEvent.isCorrect()})
 * and maps matching implementations to machine enum constants via type-accessor methods
 * discovered from the trigger expression / receiver type — no hard-coded predicate names.
 */
public final class RichEventPredicateEvaluator {

    private RichEventPredicateEvaluator() {
    }

    public static List<String> filterEnumConstants(
            List<String> candidates,
            String constraint,
            String machineEnumFqn,
            TriggerPoint trigger,
            CodebaseContext context) {
        if (candidates == null || candidates.isEmpty() || constraint == null || context == null || trigger == null) {
            return List.of();
        }
        List<EnumMemberPredicateEvaluator.PredicateCall> predicates =
                EnumMemberPredicateEvaluator.extractPredicateCalls(constraint);
        if (predicates.isEmpty()) {
            return List.of();
        }
        EnumMemberPredicateEvaluator.PredicateCall predicate = predicates.get(0);
        String receiverTypeFqn = resolveReceiverTypeFqn(trigger, predicate.receiverName(), context);
        if (receiverTypeFqn == null) {
            return List.of();
        }
        Set<String> ownerTypes = new LinkedHashSet<>();
        ownerTypes.add(receiverTypeFqn);
        ownerTypes.addAll(context.getImplementations(receiverTypeFqn));

        List<String> typeAccessors = typeAccessorMethods(trigger, predicate.receiverName(), receiverTypeFqn, machineEnumFqn, context);

        Set<String> matched = new LinkedHashSet<>();
        for (String ownerFqn : ownerTypes) {
            if (!predicateMatches(ownerFqn, predicate, machineEnumFqn, context)) {
                continue;
            }
            for (String accessor : typeAccessors) {
                String typeConstant = resolveDirectEnumFromPredicateMethod(ownerFqn, accessor, context);
                if (typeConstant != null && matchesCandidate(typeConstant, candidates, machineEnumFqn, context)) {
                    matched.add(canonicalize(typeConstant, machineEnumFqn, context));
                }
            }
            String directConstant = resolveDirectEnumFromPredicateMethod(ownerFqn, predicate.methodName(), context);
            if (directConstant != null && matchesCandidate(directConstant, candidates, machineEnumFqn, context)) {
                matched.add(canonicalize(directConstant, machineEnumFqn, context));
            }
        }
        List<String> filtered = new ArrayList<>();
        for (String candidate : candidates) {
            String canonical = canonicalize(candidate, machineEnumFqn, context);
            if (matched.contains(canonical)) {
                filtered.add(candidate);
            }
        }
        return filtered;
    }

    /**
     * Type-accessor method names come from the trigger event expression (dataflow), e.g.
     * {@code myEvent.getType()} → {@code getType}, falling back to parameterless methods on the
     * receiver type that return the machine enum.
     */
    private static List<String> typeAccessorMethods(
            TriggerPoint trigger,
            String receiverName,
            String receiverTypeFqn,
            String machineEnumFqn,
            CodebaseContext context) {
        LinkedHashSet<String> names = new LinkedHashSet<>();
        String event = trigger.getEvent();
        if (event != null && receiverName != null && !receiverName.isBlank()) {
            Pattern pattern = Pattern.compile(
                    Pattern.quote(receiverName) + "\\s*\\.\\s*([a-zA-Z_][\\w]*)\\s*\\(");
            Matcher matcher = pattern.matcher(event);
            while (matcher.find()) {
                names.add(matcher.group(1));
            }
        }
        if (names.isEmpty() && receiverTypeFqn != null && machineEnumFqn != null && context != null) {
            names.addAll(enumReturningMethods(receiverTypeFqn, machineEnumFqn, context));
        }
        return List.copyOf(names);
    }

    private static List<String> enumReturningMethods(
            String receiverTypeFqn,
            String machineEnumFqn,
            CodebaseContext context) {
        TypeDeclaration type = context.getTypeDeclaration(receiverTypeFqn);
        if (type == null) {
            return List.of();
        }
        TypeResolver typeResolver = new TypeResolver(context);
        LinkedHashSet<String> names = new LinkedHashSet<>();
        for (Object declObj : type.bodyDeclarations()) {
            if (!(declObj instanceof MethodDeclaration method) || method.isConstructor()) {
                continue;
            }
            if (!method.parameters().isEmpty() || method.getReturnType2() == null) {
                continue;
            }
            String returnFqn = typeResolver.resolveTypeToFqn(method.getReturnType2(), type.getRoot());
            if (returnFqn != null) {
                int generic = returnFqn.indexOf('<');
                if (generic >= 0) {
                    returnFqn = returnFqn.substring(0, generic);
                }
            }
            if (machineEnumFqn.equals(returnFqn)) {
                names.add(method.getName().getIdentifier());
            }
        }
        return List.copyOf(names);
    }

    private static String resolveReceiverTypeFqn(TriggerPoint trigger, String receiverName, CodebaseContext context) {
        if (trigger.getClassName() == null || trigger.getMethodName() == null || receiverName == null) {
            return null;
        }
        TypeDeclaration owner = context.getTypeDeclaration(trigger.getClassName());
        if (owner == null) {
            return null;
        }
        MethodDeclaration method = context.findMethodDeclaration(owner, trigger.getMethodName(), true);
        if (method == null) {
            return null;
        }
        TypeResolver typeResolver = new TypeResolver(context);
        for (Object parameter : method.parameters()) {
            if (parameter instanceof org.eclipse.jdt.core.dom.SingleVariableDeclaration variable
                    && receiverName.equals(variable.getName().getIdentifier())) {
                return typeResolver.resolveTypeToFqn(variable.getType(), owner.getRoot());
            }
        }
        return null;
    }

    private static boolean predicateMatches(
            String ownerFqn,
            EnumMemberPredicateEvaluator.PredicateCall predicate,
            String machineEnumFqn,
            CodebaseContext context) {
        TypeDeclaration type = context.getTypeDeclaration(ownerFqn);
        if (type == null) {
            return false;
        }
        MethodDeclaration method = context.findMethodDeclaration(type, predicate.methodName(), true);
        if (method == null || method.getBody() == null) {
            return false;
        }
        Boolean[] result = new Boolean[1];
        method.getBody().accept(new ASTVisitor() {
            @Override
            public boolean visit(ReturnStatement node) {
                if (result[0] == null && node.getExpression() != null) {
                    result[0] = evaluateBoolean(node.getExpression(), ownerFqn, machineEnumFqn, context);
                }
                return super.visit(node);
            }
        });
        if (result[0] == null) {
            return false;
        }
        return predicate.negated() ? !result[0] : result[0];
    }

    private static Boolean evaluateBoolean(
            Expression expression,
            String ownerFqn,
            String machineEnumFqn,
            CodebaseContext context) {
        if (expression instanceof BooleanLiteral literal) {
            return literal.booleanValue();
        }
        // Delegate chained enum predicates (e.g. getCommand().isActive()) to enum-member evaluation —
        // method names come from the AST, not a hard-coded list.
        if (expression instanceof MethodInvocation invocation
                && invocation.arguments().isEmpty()
                && invocation.getExpression() instanceof MethodInvocation typeCall
                && typeCall.arguments().isEmpty()) {
            String enumConstant = resolveDirectEnumFromPredicateMethod(
                    ownerFqn, typeCall.getName().getIdentifier(), context);
            if (enumConstant != null && machineEnumFqn != null) {
                return EnumMemberPredicateEvaluator.evaluateConstantPredicate(
                        enumConstant,
                        machineEnumFqn,
                        invocation.getName().getIdentifier(),
                        false,
                        context);
            }
        }
        if (expression instanceof InfixExpression infix && infix.getOperator() == InfixExpression.Operator.EQUALS) {
            return true;
        }
        return null;
    }

    private static String resolveDirectEnumFromPredicateMethod(
            String ownerFqn,
            String predicateMethod,
            CodebaseContext context) {
        TypeDeclaration type = context.getTypeDeclaration(ownerFqn);
        if (type == null) {
            return null;
        }
        MethodDeclaration method = context.findMethodDeclaration(type, predicateMethod, true);
        if (method == null || method.getBody() == null) {
            return null;
        }
        final String[] constant = new String[1];
        method.getBody().accept(new ASTVisitor() {
            @Override
            public boolean visit(ReturnStatement node) {
                if (constant[0] != null || node.getExpression() == null) {
                    return super.visit(node);
                }
                constant[0] = extractEnumConstant(node.getExpression());
                return super.visit(node);
            }
        });
        return constant[0];
    }

    private static String extractEnumConstant(Expression expression) {
        if (expression instanceof QualifiedName qualifiedName) {
            return qualifiedName.getFullyQualifiedName();
        }
        if (expression instanceof InfixExpression infix && infix.getOperator() == InfixExpression.Operator.EQUALS) {
            String left = extractEnumConstant(infix.getLeftOperand());
            if (left != null) {
                return left;
            }
            return extractEnumConstant(infix.getRightOperand());
        }
        if (expression instanceof MethodInvocation invocation
                && invocation.getExpression() instanceof MethodInvocation typeCall
                && typeCall.getExpression() instanceof ClassInstanceCreation creation
                && !creation.arguments().isEmpty()) {
            return extractEnumConstant((Expression) creation.arguments().get(0));
        }
        return null;
    }

    private static boolean matchesCandidate(
            String constant,
            List<String> candidates,
            String machineEnumFqn,
            CodebaseContext context) {
        if (constant == null) {
            return false;
        }
        String canonical = canonicalize(constant, machineEnumFqn, context);
        for (String candidate : candidates) {
            if (canonical.equals(canonicalize(candidate, machineEnumFqn, context))) {
                return true;
            }
        }
        return false;
    }

    private static String canonicalize(String constant, String machineEnumFqn, CodebaseContext context) {
        return MachineEnumCanonicalizer.canonicalizeLabel(constant, machineEnumFqn, context);
    }
}
