package click.kamil.springstatemachineexporter.analysis.index;

import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.jdt.core.dom.*;

import java.util.*;

@Slf4j
public class AccessorIndexBuilder {

    private final TrivialAccessorDetector detector = new TrivialAccessorDetector();
    private CodebaseContext context;

    public AccessorIndex build(CodebaseContext context) {
        this.context = context;
        Map<String, DirectTypeAccessors> direct = new HashMap<>();

        for (CompilationUnit cu : context.getCompilationUnits()) {
            String packageName = cu.getPackage() != null ? cu.getPackage().getName().getFullyQualifiedName() : "";
            for (Object typeObj : cu.types()) {
                if (typeObj instanceof TypeDeclaration td) {
                    indexTypeDeclaration(td, direct);
                } else if (typeObj instanceof RecordDeclaration rd) {
                    indexRecord(rd, packageName, direct);
                } else if (typeObj instanceof EnumDeclaration ed) {
                    indexEnum(ed, packageName, direct);
                }
            }
        }

        int blocked = direct.values().stream().mapToInt(state -> state.blockedMethods.size()).sum();

        Map<String, Map<String, AccessorSummary>> effective = new HashMap<>();
        int trivial = 0;
        for (String ownerFqn : direct.keySet()) {
            Map<String, AccessorSummary> merged = mergeEffective(ownerFqn, direct, effective, new HashSet<>());
            if (!merged.isEmpty()) {
                effective.put(ownerFqn, merged);
                trivial += merged.size();
            }
        }

        log.info(
                "Built accessor index: {} trivial accessors across {} types ({} blocked overrides)",
                trivial,
                effective.size(),
                blocked);

        this.context = null;
        return new AccessorIndex(effective, effective.size(), trivial, blocked);
    }

    private void indexTypeDeclaration(TypeDeclaration td, Map<String, DirectTypeAccessors> direct) {
        String ownerFqn = context.getFqn(td);
        DirectTypeAccessors state = direct.computeIfAbsent(ownerFqn, k -> new DirectTypeAccessors());
        for (MethodDeclaration md : td.getMethods()) {
            classifyMethod(ownerFqn, md, fieldName -> findFieldDeclaringFqn(td, fieldName), state);
        }

        for (TypeDeclaration nested : td.getTypes()) {
            indexTypeDeclaration(nested, direct);
        }
    }

    private void indexEnum(EnumDeclaration ed, String parentFqn, Map<String, DirectTypeAccessors> direct) {
        String ownerFqn = parentFqn.isEmpty() ? ed.getName().getIdentifier() : parentFqn + "." + ed.getName().getIdentifier();
        DirectTypeAccessors state = direct.computeIfAbsent(ownerFqn, k -> new DirectTypeAccessors());

        for (Object decl : ed.bodyDeclarations()) {
            if (decl instanceof MethodDeclaration md) {
                classifyMethod(ownerFqn, md, fieldName -> findEnumFieldDeclaringFqn(ed, ownerFqn, fieldName), state);
            } else if (decl instanceof TypeDeclaration nestedTd) {
                indexTypeDeclaration(nestedTd, direct);
            } else if (decl instanceof RecordDeclaration nestedRd) {
                indexRecord(nestedRd, ownerFqn, direct);
            }
        }
    }

    private void indexRecord(RecordDeclaration rd, String parentFqn, Map<String, DirectTypeAccessors> direct) {
        String ownerFqn = parentFqn.isEmpty() ? rd.getName().getIdentifier() : parentFqn + "." + rd.getName().getIdentifier();
        DirectTypeAccessors state = direct.computeIfAbsent(ownerFqn, k -> new DirectTypeAccessors());

        for (Object compObj : rd.recordComponents()) {
            String componentName = recordComponentName(compObj);
            if (componentName == null) {
                continue;
            }
            detector.detectRecordComponent(ownerFqn, componentName).ifPresent(summary ->
                    state.trivial.put(summary.methodName(), summary));
        }

        TrivialAccessorDetector.FieldLookup recordFieldLookup =
                fieldName -> findRecordFieldDeclaringFqn(rd, ownerFqn, fieldName);

        for (Object declObj : rd.bodyDeclarations()) {
            if (declObj instanceof MethodDeclaration md) {
                String methodName = md.getName().getIdentifier();
                AccessorSummary existing = state.trivial.get(methodName);
                if (existing != null && existing.kind() == AccessorKind.RECORD_COMPONENT) {
                    continue;
                }
                classifyMethod(ownerFqn, md, recordFieldLookup, state);
            } else if (declObj instanceof TypeDeclaration nestedTd) {
                indexTypeDeclaration(nestedTd, direct);
            } else if (declObj instanceof RecordDeclaration nestedRd) {
                indexRecord(nestedRd, ownerFqn, direct);
            }
        }
    }

    private void classifyMethod(
            String ownerFqn,
            MethodDeclaration md,
            TrivialAccessorDetector.FieldLookup fieldLookup,
            DirectTypeAccessors state) {
        if (md.getBody() == null) {
            return;
        }

        String methodName = md.getName().getIdentifier();
        Optional<AccessorSummary> summary = detector.detect(ownerFqn, md, fieldLookup);
        applyClassification(state, methodName, summary);
    }

    private void applyClassification(DirectTypeAccessors state, String methodName, Optional<AccessorSummary> summary) {
        if (summary.isPresent()) {
            state.trivial.put(methodName, summary.get());
            state.blockedMethods.remove(methodName);
            return;
        }

        if (looksLikeAccessorMethod(methodName)) {
            state.blockedMethods.add(methodName);
            state.trivial.remove(methodName);
        }
    }

    private boolean looksLikeAccessorMethod(String methodName) {
        return methodName.startsWith("get")
                || methodName.startsWith("set")
                || methodName.startsWith("is");
    }

    private Map<String, AccessorSummary> mergeEffective(
            String ownerFqn,
            Map<String, DirectTypeAccessors> direct,
            Map<String, Map<String, AccessorSummary>> cache,
            Set<String> visiting) {
        ownerFqn = resolveTypeFqn(ownerFqn);
        if (ownerFqn == null) {
            return Map.of();
        }
        if (cache.containsKey(ownerFqn)) {
            return cache.get(ownerFqn);
        }
        if (!visiting.add(ownerFqn)) {
            return Map.of();
        }

        Map<String, AccessorSummary> merged = new HashMap<>();
        TypeDeclaration td = context.getTypeDeclaration(ownerFqn);
        if (td != null) {
            String superFqn = resolveTypeFqn(context.getSuperclassFqn(td));
            if (superFqn != null && !"java.lang.Object".equals(superFqn)) {
                for (Map.Entry<String, AccessorSummary> inherited : mergeEffective(superFqn, direct, cache, visiting).entrySet()) {
                    merged.put(inherited.getKey(), rebindOwner(inherited.getValue(), ownerFqn));
                }
            }
        }

        DirectTypeAccessors own = direct.get(ownerFqn);
        if (own != null) {
            own.blockedMethods.forEach(merged::remove);
            merged.putAll(own.trivial);
        }

        visiting.remove(ownerFqn);
        cache.put(ownerFqn, Map.copyOf(merged));
        return merged;
    }

    private AccessorSummary rebindOwner(AccessorSummary summary, String ownerFqn) {
        if (summary.ownerFqn().equals(ownerFqn)) {
            return summary;
        }
        return new AccessorSummary(
                ownerFqn,
                summary.methodName(),
                summary.kind(),
                summary.fieldName(),
                summary.paramName(),
                summary.declaringFqn(),
                summary.returnsThis()
        );
    }

    private String resolveTypeFqn(String typeName) {
        if (typeName == null || typeName.isEmpty()) {
            return null;
        }
        if (typeName.contains(".")) {
            return typeName;
        }
        TypeDeclaration td = context.getTypeDeclaration(typeName);
        return td != null ? context.getFqn(td) : typeName;
    }

    private static final class DirectTypeAccessors {
        private final Map<String, AccessorSummary> trivial = new HashMap<>();
        private final Set<String> blockedMethods = new HashSet<>();
    }

    private Optional<String> findFieldDeclaringFqn(TypeDeclaration td, String fieldName) {
        return findFieldDeclaringFqn(td, fieldName, new HashSet<>());
    }

    private Optional<String> findFieldDeclaringFqn(TypeDeclaration td, String fieldName, Set<String> visited) {
        if (td == null) {
            return Optional.empty();
        }
        String ownerFqn = context.getFqn(td);
        if (!visited.add(ownerFqn)) {
            return Optional.empty();
        }

        for (FieldDeclaration fd : td.getFields()) {
            for (Object fragObj : fd.fragments()) {
                VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
                if (frag.getName().getIdentifier().equals(fieldName)) {
                    return Optional.of(ownerFqn);
                }
            }
        }

        String superFqn = context.getSuperclassFqn(td);
        if (superFqn != null && !"java.lang.Object".equals(superFqn)) {
            TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
            if (superTd != null) {
                return findFieldDeclaringFqn(superTd, fieldName, visited);
            }
        }
        return Optional.empty();
    }

    private Optional<String> findEnumFieldDeclaringFqn(EnumDeclaration ed, String ownerFqn, String fieldName) {
        for (Object decl : ed.bodyDeclarations()) {
            if (decl instanceof FieldDeclaration fd) {
                for (Object fragObj : fd.fragments()) {
                    VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
                    if (frag.getName().getIdentifier().equals(fieldName)) {
                        return Optional.of(ownerFqn);
                    }
                }
            }
        }
        return Optional.empty();
    }

    private Optional<String> findRecordFieldDeclaringFqn(RecordDeclaration rd, String ownerFqn, String fieldName) {
        for (Object compObj : rd.recordComponents()) {
            String componentName = recordComponentName(compObj);
            if (fieldName.equals(componentName)) {
                return Optional.of(ownerFqn);
            }
        }
        return Optional.empty();
    }

    private String recordComponentName(Object compObj) {
        if (compObj instanceof SingleVariableDeclaration svd) {
            return svd.getName().getIdentifier();
        }
        try {
            java.lang.reflect.Method getNameMethod = compObj.getClass().getMethod("getName");
            Object nameObj = getNameMethod.invoke(compObj);
            if (nameObj instanceof SimpleName sn) {
                return sn.getIdentifier();
            }
        } catch (ReflectiveOperationException ignored) {
            // fall through
        }
        return null;
    }
}
