package click.kamil.springstatemachineexporter.ast.common;

import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.ArrayType;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.MemberValuePair;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.NormalAnnotation;
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
import org.eclipse.jdt.core.dom.NameQualifiedType;
import org.eclipse.jdt.core.dom.ParameterizedType;
import org.eclipse.jdt.core.dom.PrimitiveType;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.QualifiedType;
import org.eclipse.jdt.core.dom.SimpleType;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.TypeDeclaration;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

public final class AstUtils {
    private AstUtils() {
    }

    public static MethodDeclaration findEnclosingMethod(ASTNode node) {
        ASTNode parent = node.getParent();
        while (parent != null && !(parent instanceof MethodDeclaration)) {
            parent = parent.getParent();
        }
        return (MethodDeclaration) parent;
    }

    public static TypeDeclaration findEnclosingType(ASTNode node) {
        ASTNode parent = node.getParent();
        while (parent != null && !(parent instanceof TypeDeclaration)) {
            parent = parent.getParent();
        }
        return (TypeDeclaration) parent;
    }

    public static String simpleName(String fqn) {
        if (fqn == null || fqn.isEmpty()) {
            return fqn;
        }
        int dot = fqn.lastIndexOf('.');
        return dot >= 0 ? fqn.substring(dot + 1) : fqn;
    }

    public static String qualifier(String fqn) {
        if (fqn == null || !fqn.contains(".")) {
            return null;
        }
        return fqn.substring(0, fqn.lastIndexOf('.'));
    }

    public static String extractAnnotationMember(Annotation annotation, String memberName) {
        if (annotation instanceof SingleMemberAnnotation sma) {
            if ("value".equals(memberName)) {
                return sma.getValue().toString();
            }
        } else if (annotation instanceof NormalAnnotation na) {
            for (Object pairObj : na.values()) {
                MemberValuePair pair = (MemberValuePair) pairObj;
                if (pair.getName().getIdentifier().equals(memberName)) {
                    return pair.getValue().toString();
                }
            }
        }
        return "";
    }

    public static String extractSimpleTypeName(Type type) {
        if (type.isSimpleType()) {
            Name name = ((SimpleType) type).getName();
            if (name instanceof QualifiedName qn) {
                return qn.getName().getIdentifier();
            }
            return name.toString();
        } else if (type.isParameterizedType()) {
            Type rawType = ((ParameterizedType) type).getType();
            return extractSimpleTypeName(rawType);
        } else if (type.isQualifiedType()) {
            return ((QualifiedType) type).getName().getIdentifier();
        } else if (type.isNameQualifiedType()) {
            return ((NameQualifiedType) type).getName().getIdentifier();
        } else if (type.isArrayType()) {
            return extractSimpleTypeName(((ArrayType) type).getElementType());
        } else if (type.isPrimitiveType()) {
            return ((PrimitiveType) type).getPrimitiveTypeCode().toString();
        } else {
            return type.toString(); // fallback
        }
    }

    public static boolean isFunctionalCollectionMethod(org.eclipse.jdt.core.dom.MethodInvocation node) {
        if (node.getExpression() == null) {
            return false;
        }

        org.eclipse.jdt.core.dom.ITypeBinding typeBinding = node.getExpression().resolveTypeBinding();
        if (typeBinding != null) {
            if (isCollectionOrStreamType(typeBinding)) {
                return true;
            }
            return false;
        }
        
        String methodName = node.getName().getIdentifier();
        if (methodName.equals("forEach") || methodName.equals("map") || 
            methodName.equals("filter") || methodName.equals("flatMap") || 
            methodName.equals("anyMatch") || methodName.equals("allMatch") || 
            methodName.equals("noneMatch") || methodName.equals("reduce") ||
            methodName.equals("collect") || methodName.equals("removeIf") ||
            methodName.equals("ifPresent") || methodName.equals("ifPresentOrElse") ||
            methodName.equals("compute") || methodName.equals("computeIfAbsent") ||
            methodName.equals("computeIfPresent") || methodName.equals("replaceAll") ||
            methodName.equals("merge") || methodName.equals("peek") ||
            methodName.equals("doOnNext") || methodName.equals("doOnSuccess") ||
            methodName.equals("doOnError") || methodName.equals("concatMap") ||
            methodName.equals("switchMap")) {
            return true;
        }
        
        return false; 
    }

    private static boolean isCollectionOrStreamType(org.eclipse.jdt.core.dom.ITypeBinding typeBinding) {
        if (typeBinding == null) return false;
        String fqn = typeBinding.getErasure().getQualifiedName();
        if (fqn.startsWith("java.util.stream.") || 
            fqn.equals("java.lang.Iterable") || 
            fqn.equals("java.util.Collection") || 
            fqn.equals("java.util.List") || 
            fqn.equals("java.util.Set") || 
            fqn.equals("java.util.Map") || 
            fqn.equals("java.util.Iterator") || 
            fqn.equals("java.util.Optional") ||
            fqn.equals("reactor.core.publisher.Flux") ||
            fqn.equals("reactor.core.publisher.Mono")) {
            return true;
        }
        
        for (org.eclipse.jdt.core.dom.ITypeBinding intf : typeBinding.getInterfaces()) {
            if (isCollectionOrStreamType(intf)) return true;
        }
        
        if (typeBinding.getSuperclass() != null) {
            return isCollectionOrStreamType(typeBinding.getSuperclass());
        }
        
        return false;
    }

    public static String findConditionConstraint(org.eclipse.jdt.core.dom.ASTNode node) {
        java.util.List<String> conditions = new java.util.ArrayList<>();
        org.eclipse.jdt.core.dom.ASTNode current = node;
        while (current != null && !(current instanceof org.eclipse.jdt.core.dom.MethodDeclaration)) {
            org.eclipse.jdt.core.dom.ASTNode parent = current.getParent();
            if (parent instanceof org.eclipse.jdt.core.dom.IfStatement ifStmt) {
                if (ifStmt.getExpression() != current) {
                    org.eclipse.jdt.core.dom.Expression cond = ifStmt.getExpression();
                    if (current == ifStmt.getElseStatement()) {
                        conditions.add("!(" + cond.toString() + ")");
                    } else {
                        conditions.add(cond.toString());
                    }
                }
            }
            current = parent;
        }
        String switchConstraint = buildSwitchCaseConstraint(findEnclosingSwitchCase(node));
        if (switchConstraint != null) {
            conditions.add(switchConstraint);
        }
        if (conditions.isEmpty()) return null;
        java.util.Collections.reverse(conditions);
        return String.join(" && ", conditions);
    }

    static org.eclipse.jdt.core.dom.SwitchCase findEnclosingSwitchCase(org.eclipse.jdt.core.dom.ASTNode node) {
        org.eclipse.jdt.core.dom.ASTNode current = node;
        while (current != null && !(current instanceof org.eclipse.jdt.core.dom.MethodDeclaration)) {
            if (current instanceof org.eclipse.jdt.core.dom.SwitchCase switchCase) {
                return switchCase;
            }
            current = current.getParent();
        }
        current = node;
        while (current != null && !(current instanceof org.eclipse.jdt.core.dom.MethodDeclaration)) {
            if (current instanceof org.eclipse.jdt.core.dom.YieldStatement yieldStatement) {
                org.eclipse.jdt.core.dom.ASTNode parent = yieldStatement.getParent();
                if (parent instanceof org.eclipse.jdt.core.dom.SwitchStatement switchStatement) {
                    org.eclipse.jdt.core.dom.SwitchCase switchCase =
                            findSwitchCaseForStatement(switchStatement.statements(), yieldStatement);
                    if (switchCase != null) {
                        return switchCase;
                    }
                } else if (parent instanceof org.eclipse.jdt.core.dom.SwitchExpression switchExpression) {
                    org.eclipse.jdt.core.dom.SwitchCase switchCase =
                            findSwitchCaseForStatement(switchExpression.statements(), yieldStatement);
                    if (switchCase != null) {
                        return switchCase;
                    }
                }
            } else if (current instanceof org.eclipse.jdt.core.dom.ExpressionStatement expressionStatement) {
                org.eclipse.jdt.core.dom.ASTNode parent = expressionStatement.getParent();
                if (parent instanceof org.eclipse.jdt.core.dom.SwitchCase switchCase) {
                    return switchCase;
                }
            }
            current = current.getParent();
        }
        return null;
    }

    private static org.eclipse.jdt.core.dom.SwitchCase findSwitchCaseForStatement(
            java.util.List<?> statements, org.eclipse.jdt.core.dom.ASTNode bodyStatement) {
        int idx = statements.indexOf(bodyStatement);
        if (idx < 0) {
            return null;
        }
        for (int i = idx - 1; i >= 0; i--) {
            if (statements.get(i) instanceof org.eclipse.jdt.core.dom.SwitchCase switchCase) {
                return switchCase;
            }
        }
        return null;
    }

    private static String buildSwitchCaseConstraint(org.eclipse.jdt.core.dom.SwitchCase switchCase) {
        if (switchCase == null || switchCase.isDefault()) {
            return null;
        }
        org.eclipse.jdt.core.dom.Expression switchSelector = null;
        org.eclipse.jdt.core.dom.ASTNode switchParent = switchCase.getParent();
        if (switchParent instanceof org.eclipse.jdt.core.dom.SwitchStatement ss) {
            switchSelector = ss.getExpression();
        } else if (switchParent instanceof org.eclipse.jdt.core.dom.SwitchExpression se) {
            switchSelector = se.getExpression();
        }
        if (switchSelector == null) {
            return null;
        }
        java.util.List<String> caseLabels = new java.util.ArrayList<>();
        for (Object exprObj : switchCase.expressions()) {
            if (exprObj instanceof org.eclipse.jdt.core.dom.Expression caseExpr) {
                caseLabels.add(caseExpr.toString());
            }
        }
        if (caseLabels.isEmpty()) {
            return null;
        }
        String selector = switchSelector.toString();
        if (caseLabels.size() == 1) {
            return selector + " == " + caseLabels.get(0);
        }
        StringBuilder combined = new StringBuilder("(");
        for (int i = 0; i < caseLabels.size(); i++) {
            if (i > 0) {
                combined.append(" || ");
            }
            combined.append(selector).append(" == ").append(caseLabels.get(i));
        }
        combined.append(')');
        return combined.toString();
    }

    public static Expression parseExpression(String snippet) {
        if (snippet == null || snippet.isBlank()) {
            return null;
        }
        ASTParser parser = ASTParser.newParser(AST.JLS17);
        Map<String, String> options = JavaCore.getOptions();
        JavaCore.setComplianceOptions(JavaCore.VERSION_17, options);
        parser.setCompilerOptions(options);
        parser.setKind(ASTParser.K_EXPRESSION);
        parser.setSource(snippet.toCharArray());
        try {
            ASTNode node = parser.createAST(null);
            if (node instanceof Expression expression) {
                return expression;
            }
            if (node instanceof CompilationUnit) {
                ASTParser fallbackParser = ASTParser.newParser(AST.JLS17);
                fallbackParser.setCompilerOptions(options);
                fallbackParser.setKind(ASTParser.K_COMPILATION_UNIT);
                fallbackParser.setSource(("class A { Object o = " + snippet + "; }").toCharArray());
                CompilationUnit cu = (CompilationUnit) fallbackParser.createAST(null);
                if (!cu.types().isEmpty() && cu.types().get(0) instanceof TypeDeclaration td) {
                    if (!td.bodyDeclarations().isEmpty() && td.bodyDeclarations().get(0) instanceof FieldDeclaration fd) {
                        if (!fd.fragments().isEmpty() && fd.fragments().get(0) instanceof VariableDeclarationFragment vdf) {
                            return vdf.getInitializer();
                        }
                    }
                }
            }
        } catch (RuntimeException ignored) {
            return null;
        }
        return null;
    }

    public static Expression findExpressionInMethod(String methodFqn, String expressionText, CodebaseContext context) {
        if (methodFqn == null || expressionText == null || !methodFqn.contains(".") || context == null) {
            return null;
        }
        String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
        String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
        TypeDeclaration td = context.getTypeDeclaration(className);
        if (td == null) {
            return null;
        }
        MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
        if (md == null || md.getBody() == null) {
            return null;
        }
        final Expression[] match = new Expression[1];
        md.getBody().accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
            @Override
            public boolean visit(org.eclipse.jdt.core.dom.MethodInvocation node) {
                if (expressionText.equals(node.toString())) {
                    match[0] = node;
                    return false;
                }
                return super.visit(node);
            }

            @Override
            public boolean visit(org.eclipse.jdt.core.dom.SuperMethodInvocation node) {
                if (expressionText.equals(node.toString())) {
                    match[0] = node;
                    return false;
                }
                return super.visit(node);
            }

            @Override
            public boolean visit(QualifiedName node) {
                if (expressionText.equals(node.toString())) {
                    match[0] = node;
                    return false;
                }
                return super.visit(node);
            }

            @Override
            public boolean visit(org.eclipse.jdt.core.dom.SimpleName node) {
                if (expressionText.equals(node.getIdentifier())) {
                    match[0] = node;
                    return false;
                }
                return super.visit(node);
            }
        });
        return match[0];
    }

    public static org.eclipse.jdt.core.dom.MethodInvocation findMethodInvocationInMethod(
            String methodFqn, String expressionText, CodebaseContext context) {
        Expression found = findExpressionInMethod(methodFqn, expressionText, context);
        return found instanceof org.eclipse.jdt.core.dom.MethodInvocation mi ? mi : null;
    }

    public static Expression findExpressionInMethods(
            List<String> methodFqns, String expressionText, CodebaseContext context) {
        if (methodFqns == null || expressionText == null) {
            return null;
        }
        for (String methodFqn : methodFqns) {
            Expression found = findExpressionInMethod(methodFqn, expressionText, context);
            if (found != null) {
                return found;
            }
        }
        return null;
    }

    public static String combineExpressionText(String base, String suffix) {
        if (suffix == null || suffix.isEmpty()) {
            return base;
        }
        return base + suffix;
    }

    public static String toParseSnippet(String boundValue) {
        if (boundValue == null || boundValue.isBlank()) {
            return boundValue;
        }
        if (boundValue.startsWith("\"") && boundValue.endsWith("\"")) {
            return boundValue;
        }
        if (isEnumLikeConstantName(boundValue)) {
            return boundValue;
        }
        return "\"" + boundValue.replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
    }

    /**
     * Analysis-only tokens that must never be treated as concrete enum/string proofs.
     * Includes {@code ENUM_SET:…}, {@code <SYMBOLIC:…>}, and wildcard tails like {@code .*>}.
     */
    public static boolean isAnalysisToken(String value) {
        if (value == null || value.isEmpty()) {
            return false;
        }
        if (value.startsWith("ENUM_SET:") || value.startsWith("<SYMBOLIC:")) {
            return true;
        }
        if (value.contains("SYMBOLIC:")) {
            return true;
        }
        return value.endsWith(".*>");
    }

    /** True when {@code value} is a concrete constant suitable for binding storage / constraint substitution. */
    public static boolean isConcreteConstant(String value) {
        if (value == null || value.isBlank() || isAnalysisToken(value)) {
            return false;
        }
        return true;
    }

    public static String formatEnumSet(Collection<String> values) {
        if (values == null || values.isEmpty()) {
            return null;
        }
        return "ENUM_SET:" + String.join(",", values);
    }

    /**
     * @return elements inside an {@code ENUM_SET:…} token, or empty if not an enum-set token
     */
    public static List<String> parseEnumSet(String value) {
        if (value == null || !value.startsWith("ENUM_SET:")) {
            return List.of();
        }
        String body = value.substring("ENUM_SET:".length());
        if (body.isBlank()) {
            return List.of();
        }
        List<String> out = new ArrayList<>();
        for (String part : body.split(",")) {
            if (part != null && !part.isBlank()) {
                out.add(part);
            }
        }
        return out;
    }

    public static boolean isEnumLikeConstantName(String value) {
        if (value == null || value.isEmpty() || isAnalysisToken(value)) {
            return false;
        }
        int dot = value.lastIndexOf('.');
        if (dot <= 0 || dot >= value.length() - 1) {
            return false;
        }
        return Character.isUpperCase(value.charAt(dot + 1));
    }
}
