package click.kamil.springstatemachineexporter.analysis.pipeline;

import click.kamil.springstatemachineexporter.analysis.index.AccessorKind;
import click.kamil.springstatemachineexporter.analysis.index.AccessorFieldResolver;
import click.kamil.springstatemachineexporter.analysis.index.AccessorSummary;
import click.kamil.springstatemachineexporter.analysis.index.GetterChainEdge;
import click.kamil.springstatemachineexporter.analysis.pipeline.GetterBodyConstantScanner;
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
import click.kamil.springstatemachineexporter.analysis.service.ConstructorAnalyzer;
import click.kamil.springstatemachineexporter.analysis.service.ConstantExtractor;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.ReturnStatement;
import org.eclipse.jdt.core.dom.TypeDeclaration;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiConsumer;

/**
 * Unified accessor resolution pipeline: index lookup first, then field/CIC/method-body fallbacks.
 */
public final class AccessorResolver {

    public record GetterChainStep(String typeFqn, ClassInstanceCreation cic) {
    }

    public record ReceiverContext(
            TypeDeclaration ownerType,
            ClassInstanceCreation cic,
            CompilationUnit contextCu,
            boolean anonymousGetter,
            Map<String, String> fieldValues) {
    }

    private final CodebaseContext context;
    private final ConstantExtractor constantExtractor;
    private final ConstructorAnalyzer constructorAnalyzer;
    private final ConstantResolver constantResolver;
    private final FieldInitializerFinder fieldInitializerFinder;
    private final Map<String, List<String>> sessionCache = new HashMap<>();
    private final Map<String, Optional<AccessorSummary>> indexedGetterCache = new HashMap<>();

    public AccessorResolver(
            CodebaseContext context,
            ConstantExtractor constantExtractor,
            ConstructorAnalyzer constructorAnalyzer,
            ConstantResolver constantResolver) {
        this.context = context;
        this.constantExtractor = constantExtractor;
        this.constructorAnalyzer = constructorAnalyzer;
        this.constantResolver = constantResolver;
        this.fieldInitializerFinder = new FieldInitializerFinder(context);
    }

    public Optional<AccessorSummary> lookupIndexedGetter(String className, String methodName) {
        if (className == null || className.isBlank()) {
            return Optional.empty();
        }
        String cacheKey = className + "#" + methodName;
        if (indexedGetterCache.containsKey(cacheKey)) {
            return indexedGetterCache.get(cacheKey);
        }
        Optional<AccessorSummary> resolved = lookupIndexedGetterInternal(className, methodName);
        indexedGetterCache.put(cacheKey, resolved);
        return resolved;
    }

    private Optional<AccessorSummary> lookupIndexedGetterInternal(String className, String methodName) {
        Optional<AccessorSummary> direct = context.getAccessorIndex().lookup(className, methodName);
        if (direct.isPresent()) {
            return direct;
        }
        TypeDeclaration typeDeclaration = context.getTypeDeclaration(className);
        if (typeDeclaration != null) {
            direct = context.getAccessorIndex().lookup(context.getFqn(typeDeclaration), methodName);
            if (direct.isPresent()) {
                return direct;
            }
            if (!typeDeclaration.isInterface() && !org.eclipse.jdt.core.dom.Modifier.isAbstract(typeDeclaration.getModifiers())) {
                return Optional.empty();
            }
        }
        List<String> implementations = context.getImplementations(className);
        if (implementations == null || implementations.isEmpty()) {
            return Optional.empty();
        }
        Optional<AccessorSummary> precomputed = context.getPolymorphicAccessorIndex()
                .representativeImplAccessor(className, methodName);
        if (precomputed.isPresent()) {
            return precomputed;
        }
        for (String implementation : implementations) {
            Optional<AccessorSummary> implAccessor = context.getAccessorIndex().lookup(implementation, methodName);
            if (implAccessor.isPresent()) {
                return implAccessor;
            }
        }
        return Optional.empty();
    }

    public List<String> resolve(String ownerFqn, String methodName, ReceiverContext receiverContext, ResolutionBudget budget) {
        return resolve(ownerFqn, methodName, receiverContext, budget, new HashSet<>());
    }

    public List<String> resolve(
            String ownerFqn,
            String methodName,
            ReceiverContext receiverContext,
            ResolutionBudget budget,
            Set<String> visited) {
        if (ownerFqn == null || methodName == null) {
            return List.of();
        }
        if (budget == null) {
            budget = ResolutionBudget.defaults();
        }

        String cacheKey = cacheKey(ownerFqn, methodName, receiverContext);
        List<String> cached = sessionCache.get(cacheKey);
        if (cached != null) {
            return cached;
        }

        List<String> resolved = resolveInternal(ownerFqn, methodName, receiverContext, budget, visited);
        if (!resolved.isEmpty() || isStaticCacheable(receiverContext)) {
            List<String> cachedCopy = List.copyOf(resolved);
            sessionCache.put(cacheKey, cachedCopy);
            return cachedCopy;
        }
        return resolved;
    }

    public GetterChainStep resolveGetterChainStep(String ownerFqn, String getterName, ResolutionBudget budget) {
        if (ownerFqn == null || getterName == null) {
            return null;
        }

        Optional<GetterChainEdge> precomputed = context.getGetterChainIndex().lookup(ownerFqn, getterName);
        if (precomputed.isPresent()) {
            GetterChainEdge edge = precomputed.get();
            return new GetterChainStep(edge.nextTypeFqn(), edge.fieldInitializerCic());
        }

        Optional<AccessorSummary> indexedAccessor = lookupIndexedGetter(ownerFqn, getterName);
        if (indexedAccessor.isPresent() && indexedAccessor.get().isGetter()) {
            AccessorSummary accessor = indexedAccessor.get();
            ClassInstanceCreation nextCic = fieldInitializerFinder.findFieldInitializerCic(ownerFqn, accessor.fieldName());
            String nextType = fieldInitializerFinder.resolveFieldTypeName(ownerFqn, accessor.fieldName());
            if (nextCic != null) {
                nextType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(nextCic.getType());
            }
            if (nextType != null) {
                return new GetterChainStep(nextType, nextCic);
            }
        }

        String simpleType = simplifyTypeName(ownerFqn);
        ClassInstanceCreation nextCic = constructorAnalyzer.findReturnedCIC(simpleType, getterName, context);
        if (nextCic == null) {
            String fieldName = indexedFieldName(ownerFqn, getterName);
            nextCic = fieldInitializerFinder.findFieldInitializerCic(ownerFqn, fieldName);
        }
        if (nextCic != null) {
            return new GetterChainStep(
                    click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(nextCic.getType()),
                    nextCic);
        }

        TypeDeclaration td = context.getTypeDeclaration(simpleType);
        if (td == null) {
            td = context.getTypeDeclaration(ownerFqn);
        }
        if (td != null) {
            MethodDeclaration getter = context.findMethodDeclaration(td, getterName, true);
            if (getter != null && getter.getReturnType2() != null) {
                return new GetterChainStep(getter.getReturnType2().toString(), null);
            }
        }
        return null;
    }

    private static boolean isStaticCacheable(ReceiverContext receiverContext) {
        if (receiverContext == null) {
            return true;
        }
        if (receiverContext.cic() != null) {
            return false;
        }
        Map<String, String> fieldValues = receiverContext.fieldValues();
        return fieldValues == null || fieldValues.isEmpty();
    }

    public void clearSessionCache() {
        sessionCache.clear();
        indexedGetterCache.clear();
    }

    private List<String> resolveInternal(
            String ownerFqn,
            String methodName,
            ReceiverContext receiverContext,
            ResolutionBudget budget,
            Set<String> visited) {
        TypeDeclaration ownerType = receiverContext != null ? receiverContext.ownerType() : null;
        if (ownerType == null) {
            ownerType = context.getTypeDeclaration(ownerFqn);
        }
        boolean widenToImplementations = ownerType == null
                || ownerType.isInterface()
                || org.eclipse.jdt.core.dom.Modifier.isAbstract(ownerType.getModifiers());

        if (widenToImplementations) {
            List<String> results = new ArrayList<>();

            if (receiverContext != null && receiverContext.cic() != null) {
                String cicOwnerFqn = resolveCicOwnerFqn(receiverContext);
                if (cicOwnerFqn != null) {
                    TypeDeclaration cicOwnerType = context.getTypeDeclaration(
                            cicOwnerFqn, receiverContext.contextCu());
                    Optional<AccessorSummary> indexedAccessor = lookupIndexedGetter(cicOwnerFqn, methodName);
                    ReceiverContext cicReceiverContext = new ReceiverContext(
                            cicOwnerType,
                            receiverContext.cic(),
                            receiverContext.contextCu(),
                            receiverContext.anonymousGetter(),
                            receiverContext.fieldValues());
                    List<String> cicValues = resolveWithCic(
                            cicOwnerFqn,
                            methodName,
                            cicReceiverContext,
                            indexedAccessor.orElse(null),
                            visited);
                    if (!cicValues.isEmpty()) {
                        return deduplicatePreservingOrder(cicValues);
                    }
                }
                return List.of();
            }

            if (constantExtractor != null && !budget.isMethodReturnExhausted(0)) {
                CompilationUnit contextCu = receiverContext != null ? receiverContext.contextCu() : null;
                results.addAll(constantExtractor.resolveMethodReturnConstant(
                        ownerFqn, methodName, 0, visited, contextCu, budget));
            }
            if (!results.isEmpty()) {
                return deduplicatePreservingOrder(results);
            }
            return List.of();
        }

        Optional<AccessorSummary> indexedAccessor = lookupIndexedGetter(ownerFqn, methodName);
        if (indexedAccessor.isPresent() && indexedAccessor.get().isGetter()) {
            List<String> indexed = resolveIndexedAccessor(indexedAccessor.get(), receiverContext, visited);
            if (!indexed.isEmpty()) {
                return indexed;
            }
        }

        if (receiverContext != null && receiverContext.ownerType() != null && receiverContext.cic() != null) {
            List<String> cicValues = resolveWithCic(
                    ownerFqn, methodName, receiverContext, indexedAccessor.orElse(null), visited);
            if (!cicValues.isEmpty()) {
                return cicValues;
            }
        } else if (receiverContext != null && receiverContext.cic() != null) {
            ReceiverContext enriched = new ReceiverContext(
                    context.getTypeDeclaration(ownerFqn),
                    receiverContext.cic(),
                    receiverContext.contextCu(),
                    receiverContext.anonymousGetter(),
                    receiverContext.fieldValues());
            List<String> cicValues = resolveWithCic(
                    ownerFqn, methodName, enriched, indexedAccessor.orElse(null), visited);
            if (!cicValues.isEmpty()) {
                return cicValues;
            }
        }

        if (constantExtractor != null && !budget.isMethodReturnExhausted(0)) {
            CompilationUnit contextCu = receiverContext != null ? receiverContext.contextCu() : null;
            List<String> deep = constantExtractor.resolveMethodReturnConstant(
                    ownerFqn, methodName, 0, visited, contextCu, budget);
            if (!deep.isEmpty()) {
                return deep;
            }
        }
        return List.of();
    }

    private List<String> resolveIndexedAccessor(
            AccessorSummary accessor,
            ReceiverContext receiverContext,
            Set<String> visited) {
        if (accessor.kind() == AccessorKind.CONSTANT_GETTER) {
            List<String> constants = extractConstantGetterReturn(accessor.ownerFqn(), accessor.methodName());
            if (!constants.isEmpty()) {
                return constants;
            }
            if (constantExtractor != null) {
                CompilationUnit contextCu = receiverContext != null ? receiverContext.contextCu() : null;
                return constantExtractor.resolveMethodReturnConstant(
                        accessor.ownerFqn(), accessor.methodName(), 0, new HashSet<>(visited), contextCu);
            }
        }

        if (constantExtractor != null) {
            CompilationUnit contextCu = receiverContext != null ? receiverContext.contextCu() : null;
            String requestingTypeFqn = receiverContext != null && receiverContext.ownerType() != null
                    ? context.getFqn(receiverContext.ownerType())
                    : accessor.ownerFqn();
            List<String> fieldConstants = constantExtractor.resolveIndexedGetterFieldConstants(
                    accessor, requestingTypeFqn, contextCu, visited);
            if (!fieldConstants.isEmpty()) {
                return fieldConstants;
            }
        }

        if (receiverContext != null
                && receiverContext.fieldValues() != null
                && accessor.fieldName() != null) {
            String fieldValue = receiverContext.fieldValues().get(accessor.fieldName());
            if (fieldValue != null) {
                return List.of(fieldValue);
            }
        }
        return List.of();
    }

    private List<String> resolveWithCic(
            String ownerFqn,
            String methodName,
            ReceiverContext receiverContext,
            AccessorSummary indexedAccessor,
            Set<String> visited) {
        TypeDeclaration td = receiverContext.ownerType();
        if (td == null) {
            td = context.getTypeDeclaration(ownerFqn);
        }
        ClassInstanceCreation cic = receiverContext.cic();
        Map<String, String> fieldValues = receiverContext.fieldValues() != null
                ? receiverContext.fieldValues()
                : new HashMap<>();

        MethodDeclaration getter = null;
        if (cic.getAnonymousClassDeclaration() != null) {
            for (Object declObj : cic.getAnonymousClassDeclaration().bodyDeclarations()) {
                if (declObj instanceof MethodDeclaration md && md.getName().getIdentifier().equals(methodName)) {
                    getter = md;
                    break;
                }
            }
        }
        if (getter == null) {
            getter = context.findMethodDeclaration(td, methodName, true);
        }
        if (getter != null && getter.getBody() != null) {
            if (fieldValues.isEmpty() && constructorAnalyzer != null) {
                fieldValues.putAll(constructorAnalyzer.buildFieldValuesFromCIC(td, cic, context, null));
            }
            String result = constantResolver.evaluateMethodBodyWithLocals(getter, fieldValues, context, visited);
            if (result != null) {
                return List.of(qualifyEnumLikeResult(getter, result));
            }
        }

        if (indexedAccessor != null && indexedAccessor.isGetter() && !receiverContext.anonymousGetter()) {
            if (indexedAccessor.kind() == AccessorKind.CONSTANT_GETTER) {
                List<String> constantValues = extractConstantGetterReturn(ownerFqn, methodName);
                if (!constantValues.isEmpty()) {
                    return constantValues;
                }
            }
            if (fieldValues.isEmpty() && constructorAnalyzer != null) {
                fieldValues.putAll(constructorAnalyzer.buildFieldValuesFromCIC(td, cic, context, null));
            }
            if (indexedAccessor.fieldName() != null) {
                String indexedFieldValue = fieldValues.get(indexedAccessor.fieldName());
                if (indexedFieldValue != null) {
                    return List.of(indexedFieldValue);
                }
            }
            if (constantExtractor != null) {
                List<String> indexedConstants = constantExtractor.resolveIndexedGetterFieldConstants(
                        indexedAccessor, ownerFqn, receiverContext.contextCu(), visited);
                if (!indexedConstants.isEmpty()) {
                    return indexedConstants;
                }
            }
        }
        return List.of();
    }

    private List<String> extractConstantGetterReturn(String ownerFqn, String methodName) {
        return GetterBodyConstantScanner.extractConstantGetterReturn(
                context, constantExtractor, constantResolver, ownerFqn, methodName);
    }

    private static String qualifyEnumLikeResult(MethodDeclaration getter, String result) {
        if (result.contains(".")) {
            return result;
        }
        String returnType = getter.getReturnType2() != null ? getter.getReturnType2().toString() : null;
        if (returnType == null) {
            return result;
        }
        int lastDotRet = returnType.lastIndexOf('.');
        String simpleReturnType = lastDotRet > 0 ? returnType.substring(lastDotRet + 1) : returnType;
        if ("String".equals(simpleReturnType) || "Integer".equals(simpleReturnType)
                || "Long".equals(simpleReturnType) || "Boolean".equals(simpleReturnType)) {
            return result;
        }
        return simpleReturnType + "." + result;
    }

    private String indexedFieldName(String ownerFqn, String getterName) {
        Optional<AccessorSummary> accessorOpt = lookupIndexedGetter(ownerFqn, getterName);
        if (accessorOpt.isEmpty() || accessorOpt.get().fieldName() == null) {
            return null;
        }
        return accessorOpt.get().fieldName();
    }

    private String resolveCicOwnerFqn(ReceiverContext receiverContext) {
        if (receiverContext == null || receiverContext.cic() == null) {
            return null;
        }
        ClassInstanceCreation cic = receiverContext.cic();
        CompilationUnit contextCu = receiverContext.contextCu();
        String simpleName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
        TypeDeclaration cicType = contextCu != null
                ? context.getTypeDeclaration(simpleName, contextCu)
                : null;
        if (cicType == null) {
            cicType = context.getTypeDeclaration(simpleName);
        }
        if (cicType != null) {
            return context.getFqn(cicType);
        }
        if (cic.resolveTypeBinding() != null) {
            return cic.resolveTypeBinding().getErasure().getQualifiedName();
        }
        return simpleName;
    }

    private static List<String> deduplicatePreservingOrder(List<String> values) {
        List<String> deduped = new ArrayList<>();
        for (String value : values) {
            if (!deduped.contains(value)) {
                deduped.add(value);
            }
        }
        return deduped;
    }

    private static String cacheKey(String ownerFqn, String methodName, ReceiverContext receiverContext) {
        String cicKey = receiverContext != null && receiverContext.cic() != null
                ? receiverContext.cic().toString()
                : "";
        String fieldValuesKey = receiverContext != null && receiverContext.fieldValues() != null
                ? receiverContext.fieldValues().toString()
                : "";
        return ownerFqn + "#" + methodName + "#" + cicKey + "#" + fieldValuesKey;
    }

    private static String simplifyTypeName(String typeFqn) {
        if (typeFqn == null) {
            return null;
        }
        String simple = typeFqn.contains(".") ? typeFqn.substring(typeFqn.lastIndexOf('.') + 1) : typeFqn;
        if (simple.contains("<")) {
            simple = simple.substring(0, simple.indexOf('<'));
        }
        return simple;
    }
}
