package click.kamil.springstatemachineexporter.analysis.service;

import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.*;

import java.util.HashSet;
import java.util.Set;
import java.util.function.Function;

/**
 * Classifies trailing expression access (map lookup vs traceable accessor) using AST structure and
 * declared receiver types instead of string heuristics.
 */
public final class ExpressionAccessClassifier {

    public enum AccessKind {
        /** {@code Map.get(key)}, {@code list.get(i)} — keep intact for {@link ConstantExtractor}. */
        KEYED_LOOKUP,
        /** {@code getEvent()}, {@code supplier.get()} — split receiver/suffix for call-path tracing. */
        TRACEABLE_ACCESSOR,
        NONE
    }

    private final CodebaseContext context;
    private final VariableTracer variableTracer;
    private final ConstantResolver constantResolver;
    private final Function<String, ASTNode> expressionParser;

    public ExpressionAccessClassifier(
            CodebaseContext context,
            VariableTracer variableTracer,
            ConstantResolver constantResolver,
            Function<String, ASTNode> expressionParser) {
        this.context = context;
        this.variableTracer = variableTracer;
        this.constantResolver = constantResolver;
        this.expressionParser = expressionParser;
    }

    public AccessKind classifyTrailingAccess(String expression, String scopeMethod) {
        if (expression == null || expression.isBlank()) {
            return AccessKind.NONE;
        }
        ASTNode node = expressionParser.apply(expression);
        while (node instanceof ParenthesizedExpression pe) {
            node = pe.getExpression();
        }
        if (node instanceof ArrayAccess) {
            return AccessKind.NONE;
        }
        if (node instanceof MethodInvocation mi) {
            return classifyMethodInvocation(mi, scopeMethod);
        }
        return AccessKind.NONE;
    }

    public AccessKind classifyMethodInvocation(MethodInvocation mi, String scopeMethod) {
        if (mi == null) {
            return AccessKind.NONE;
        }
        String methodName = mi.getName().getIdentifier();
        int argCount = mi.arguments().size();

        if ("getOrDefault".equals(methodName) && argCount >= 1) {
            return AccessKind.KEYED_LOOKUP;
        }

        if ("get".equals(methodName)) {
            String receiverType = resolveReceiverType(mi.getExpression(), scopeMethod);
            if (argCount == 0) {
                if (isMapLikeType(receiverType)) {
                    return AccessKind.KEYED_LOOKUP;
                }
                return AccessKind.TRACEABLE_ACCESSOR;
            }
            if (isMapLikeType(receiverType) || isIndexedCollectionType(receiverType)) {
                return AccessKind.KEYED_LOOKUP;
            }
            if (receiverExpressionIsMapLiteral(mi.getExpression())) {
                return AccessKind.KEYED_LOOKUP;
            }
            return AccessKind.TRACEABLE_ACCESSOR;
        }

        if (argCount == 0 && (isBeanAccessorName(methodName) || isRecordStyleAccessor(methodName))) {
            return AccessKind.TRACEABLE_ACCESSOR;
        }

        return AccessKind.TRACEABLE_ACCESSOR;
    }

    public boolean isKeyedLookup(String expression, String scopeMethod) {
        return classifyTrailingAccess(expression, scopeMethod) == AccessKind.KEYED_LOOKUP;
    }

    public boolean isKeyedLookup(MethodInvocation mi, String scopeMethod) {
        return classifyMethodInvocation(mi, scopeMethod) == AccessKind.KEYED_LOOKUP;
    }

    /**
     * Whether a trailing suffix like {@code .getEvent()} or {@code .type()} should be resolved via accessor tracing.
     */
    public boolean isTraceableAccessorSuffix(String suffix) {
        if (suffix == null || !suffix.startsWith(".")) {
            return false;
        }
        String methodPart = suffix.substring(1);
        if (methodPart.endsWith("()")) {
            methodPart = methodPart.substring(0, methodPart.length() - 2);
        }
        return isBeanAccessorName(methodPart) || isRecordStyleAccessor(methodPart);
    }

    public boolean isTraceableAccessorExpression(String expression, String scopeMethod) {
        if (expression == null || expression.isBlank()) {
            return false;
        }
        AccessKind kind = classifyTrailingAccess(expression, scopeMethod);
        return kind == AccessKind.TRACEABLE_ACCESSOR;
    }

    private boolean receiverExpressionIsMapLiteral(Expression receiver) {
        if (constantResolver == null || context == null || receiver == null) {
            return false;
        }
        String resolved = constantResolver.resolve(receiver, context);
        return resolved != null && resolved.startsWith("MAP:");
    }

    private String resolveReceiverType(Expression receiver, String scopeMethod) {
        if (receiver == null) {
            return null;
        }
        if (receiverExpressionIsMapLiteral(receiver)) {
            return "Map";
        }
        if (receiver instanceof SimpleName sn) {
            String declared = lookupDeclaredType(scopeMethod, sn.getIdentifier());
            if (declared != null) {
                return declared;
            }
            return lookupFieldType(classNameFromScopeMethod(scopeMethod), sn.getIdentifier());
        }
        if (receiver instanceof ThisExpression) {
            return classNameFromScopeMethod(scopeMethod);
        }
        if (receiver instanceof FieldAccess fa) {
            String fieldName = fa.getName().getIdentifier();
            if (fa.getExpression() instanceof ThisExpression) {
                return lookupFieldType(classNameFromScopeMethod(scopeMethod), fieldName);
            }
            if (fa.getExpression() instanceof SimpleName outer) {
                String outerType = lookupDeclaredType(scopeMethod, outer.getIdentifier());
                return lookupFieldType(rawTypeName(outerType), fieldName);
            }
            if (fa.getExpression() instanceof QualifiedName qn) {
                String outerType = resolveQualifiedTypeName(qn);
                return lookupFieldType(outerType, fieldName);
            }
        }
        if (receiver instanceof QualifiedName qn) {
            String typeName = resolveQualifiedTypeName(qn);
            if (typeName != null) {
                String fieldName = qn.getName().getIdentifier();
                String fieldType = lookupFieldType(typeName, fieldName);
                if (fieldType != null) {
                    return fieldType;
                }
            }
        }
        if (receiver instanceof MethodInvocation chainMi) {
            AccessKind kind = classifyMethodInvocation(chainMi, scopeMethod);
            if (kind == AccessKind.TRACEABLE_ACCESSOR) {
                String getterName = chainMi.getName().getIdentifier();
                String baseType = resolveReceiverType(chainMi.getExpression(), scopeMethod);
                return lookupMethodReturnType(rawTypeName(baseType), getterName);
            }
        }
        return null;
    }

    private String lookupDeclaredType(String scopeMethod, String name) {
        if (variableTracer == null || scopeMethod == null || name == null) {
            return null;
        }
        return variableTracer.getVariableDeclaredType(scopeMethod, name);
    }

    private String lookupFieldType(String typeName, String fieldName) {
        if (context == null || typeName == null || fieldName == null) {
            return null;
        }
        TypeDeclaration td = context.getTypeDeclaration(typeName);
        if (td == null) {
            return null;
        }
        return findFieldType(td, fieldName, new HashSet<>());
    }

    private String findFieldType(TypeDeclaration td, String fieldName, Set<String> visited) {
        String typeFqn = context.getFqn(td);
        if (typeFqn == null || !visited.add(typeFqn)) {
            return null;
        }
        for (FieldDeclaration fd : td.getFields()) {
            for (Object fragObj : fd.fragments()) {
                VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
                if (frag.getName().getIdentifier().equals(fieldName)) {
                    return fd.getType().toString();
                }
            }
        }
        String superFqn = context.getSuperclassFqn(td);
        if (superFqn != null) {
            TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
            if (superTd != null) {
                String inherited = findFieldType(superTd, fieldName, visited);
                if (inherited != null) {
                    return inherited;
                }
            }
        }
        return null;
    }

    private String lookupMethodReturnType(String typeName, String methodName) {
        if (context == null || typeName == null || methodName == null) {
            return null;
        }
        TypeDeclaration td = context.getTypeDeclaration(typeName);
        if (td == null) {
            return null;
        }
        MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
        if (md != null && md.getReturnType2() != null) {
            return md.getReturnType2().toString();
        }
        return null;
    }

    private String resolveQualifiedTypeName(QualifiedName qn) {
        String full = qn.getFullyQualifiedName();
        if (full.contains(".")) {
            return full.substring(0, full.lastIndexOf('.'));
        }
        return classNameFromScopeMethod(null);
    }

    private static String classNameFromScopeMethod(String scopeMethod) {
        if (scopeMethod == null || !scopeMethod.contains(".")) {
            return null;
        }
        return scopeMethod.substring(0, scopeMethod.lastIndexOf('.'));
    }

    private static boolean isBeanAccessorName(String methodName) {
        return (methodName.startsWith("get") && methodName.length() > 3)
                || (methodName.startsWith("is") && methodName.length() > 2);
    }

    private static boolean isRecordStyleAccessor(String methodName) {
        return "type".equals(methodName) || "event".equals(methodName) || "name".equals(methodName);
    }

    static boolean isMapLikeType(String type) {
        String raw = rawTypeName(type);
        if (raw == null) {
            return false;
        }
        return "Map".equals(raw) || raw.endsWith("Map");
    }

    static boolean isIndexedCollectionType(String type) {
        String raw = rawTypeName(type);
        if (raw == null) {
            return false;
        }
        return "List".equals(raw) || raw.endsWith("List")
                || "Set".equals(raw) || raw.endsWith("Set")
                || raw.endsWith("Deque") || "Queue".equals(raw);
    }

    static String rawTypeName(String type) {
        if (type == null) {
            return null;
        }
        String raw = type.trim();
        if (raw.contains("<")) {
            raw = raw.substring(0, raw.indexOf('<'));
        }
        if (raw.contains(".")) {
            raw = raw.substring(raw.lastIndexOf('.') + 1);
        }
        return raw;
    }
}
