package click.kamil.springstatemachineexporter.analysis.enricher.routing;

import click.kamil.springstatemachineexporter.analysis.enricher.matching.EventMatchingEngine;
import click.kamil.springstatemachineexporter.analysis.enricher.matching.StrictFqnMatchingEngine;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import click.kamil.springstatemachineexporter.model.Transition;
import org.eclipse.jdt.core.dom.TypeDeclaration;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {

    private enum DistinctTypeAffinity {
        FOR_MACHINE,
        AGAINST_MACHINE,
        UNKNOWN
    }

    @Override
    public boolean hasProvenMachineAffinity(
            CallChain chain,
            String currentMachineName,
            CodebaseContext context,
            List<Transition> machineTransitions) {
        if (chain == null) {
            return false;
        }
        if (currentMachineName == null) {
            return isRoutedToCorrectMachine(chain, currentMachineName, context);
        }

        MachineRoutingEvidence evidence = MachineRoutingEvidence.from(chain);

        String explicitTarget = evidence.explicitBeanName();
        if (explicitTarget != null && !explicitTarget.isEmpty() && context != null) {
            Boolean namedTarget = EnableStateMachineBeanRouting.matchesNamedBean(
                    explicitTarget, currentMachineName, context);
            if (namedTarget != null) {
                return namedTarget;
            }
        }

        if (chain.getTriggerPoint() != null && context != null) {
            DistinctTypeAffinity typeAffinity =
                    resolveDistinctTypeAffinity(chain.getTriggerPoint(), currentMachineName, context);
            if (typeAffinity == DistinctTypeAffinity.FOR_MACHINE) {
                return true;
            }
            if (typeAffinity == DistinctTypeAffinity.AGAINST_MACHINE) {
                return false;
            }
        }

        if (context == null || isSingleStateMachine(context)) {
            return isRoutedToCorrectMachine(chain, currentMachineName, context);
        }

        TriggerPoint trigger = chain.getTriggerPoint();
        if (trigger != null
                && SharedServiceRoutingPolicy.isProvablySharedInfrastructure(chain)
                && matchesConfiguredTransitionEvent(trigger, machineTransitions, context)) {
            return true;
        }

        return isRoutedToCorrectMachine(chain, currentMachineName, context);
    }

    @Override
    public boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName, CodebaseContext context) {
        MachineRoutingEvidence evidence = MachineRoutingEvidence.from(chain);

        String explicitTarget = evidence.explicitBeanName();
        if (explicitTarget != null && !explicitTarget.isEmpty() && context != null && currentMachineName != null) {
            Boolean namedTarget = EnableStateMachineBeanRouting.matchesNamedBean(
                    explicitTarget, currentMachineName, context);
            if (namedTarget != null) {
                return namedTarget;
            }
        }

        boolean hasExplicitBeanTarget = explicitTarget != null && !explicitTarget.isEmpty();

        // Precise FQN Type argument match from JDT bindings at sendEvent site
        if (!hasExplicitBeanTarget && chain.getTriggerPoint() != null && context != null && currentMachineName != null) {
            String triggerEventFqn = concreteTriggerType(chain.getTriggerPoint().getEventTypeFqn());
            String triggerStateFqn = concreteTriggerType(chain.getTriggerPoint().getStateTypeFqn());
            if (triggerEventFqn != null || triggerStateFqn != null) {
                String[] machineTypes = StateMachineTypeResolver.resolve(currentMachineName, context);
                String machineStateFqn = machineTypes[0];
                String machineEventFqn = machineTypes[1];
                boolean matched = false;
                boolean mismatched = false;
                
                if (triggerEventFqn != null && machineEventFqn != null) {
                    if (typesEquivalent(triggerEventFqn, machineEventFqn)) {
                        matched = true;
                    } else if (isTypeMismatched(triggerEventFqn, machineEventFqn)) {
                        mismatched = true;
                    } else if (isSingleStateMachine(context)
                            && (isErasedOrOpaqueType(triggerEventFqn) || isErasedOrOpaqueType(machineEventFqn))) {
                        matched = true;
                    }
                }
                if (triggerStateFqn != null && machineStateFqn != null) {
                    if (typesEquivalent(triggerStateFqn, machineStateFqn)) {
                        matched = true;
                    } else if (isTypeMismatched(triggerStateFqn, machineStateFqn)) {
                        mismatched = true;
                    } else if (isSingleStateMachine(context)
                            && (isErasedOrOpaqueType(triggerStateFqn) || isErasedOrOpaqueType(machineStateFqn))) {
                        matched = true;
                    }
                }
                if (matched) {
                    if (!isAmbiguousSharedGenericMatch(
                            triggerEventFqn, triggerStateFqn, machineStateFqn, machineEventFqn)
                            || isSingleStateMachine(context)) {
                        return true;
                    }
                }
                if (mismatched) {
                    return false;
                }
                // Fail closed only when both trigger and machine generic types were resolved but
                // could not be reconciled. When machine config AST does not expose type args (stub
                // config classes in tests, generated configs), fall through to single-SM / package routing.
                boolean machineTypesKnown = machineEventFqn != null || machineStateFqn != null;
                if (machineTypesKnown) {
                    return false;
                }
            }
            // Non-null-but-ambiguous type vars ("E"/"S") are ignored above — fall through so
            // polymorphicEvents / shared-infrastructure / package evidence can decide.
        }

        if (!isSingleStateMachine(context) && isAmbiguousSharedGenericTrigger(chain.getTriggerPoint())) {
            Boolean injectionMatch = SpringInjectionRouting.matchesMachineConfig(
                    chain.getTriggerPoint(), currentMachineName, context);
            if (injectionMatch != null) {
                return injectionMatch;
            }
            // Ambiguous generics without injection evidence: do not hard-fail here when the trigger
            // still has concrete polymorphicEvents — callers with transition lists use
            // hasProvenMachineAffinity + SharedServiceRoutingPolicy for that case.
            if (!hasConcretePolymorphicEvents(chain.getTriggerPoint())) {
                return false;
            }
        }

        Boolean packageMatch = PackageNameRoutingHeuristics.matches(chain, currentMachineName, explicitTarget);
        if (packageMatch != null) {
            return packageMatch;
        }
        return context == null || isSingleStateMachine(context);
    }

    private DistinctTypeAffinity resolveDistinctTypeAffinity(
            TriggerPoint trigger, String machineName, CodebaseContext context) {
        String triggerEventFqn = concreteTriggerType(trigger.getEventTypeFqn());
        String triggerStateFqn = concreteTriggerType(trigger.getStateTypeFqn());
        if (triggerEventFqn == null && triggerStateFqn == null) {
            return DistinctTypeAffinity.UNKNOWN;
        }

        String[] machineTypes = StateMachineTypeResolver.resolve(machineName, context);
        String machineStateFqn = machineTypes[0];
        String machineEventFqn = machineTypes[1];

        boolean matched = false;
        boolean mismatched = false;

        if (triggerEventFqn != null && machineEventFqn != null) {
            if (typesEquivalent(triggerEventFqn, machineEventFqn)
                    && !isAmbiguousSharedGenericType(triggerEventFqn)) {
                matched = true;
            } else if (isDistinctTypeMismatched(triggerEventFqn, machineEventFqn)) {
                mismatched = true;
            }
        }
        if (triggerStateFqn != null && machineStateFqn != null) {
            if (typesEquivalent(triggerStateFqn, machineStateFqn)
                    && !isAmbiguousSharedGenericType(triggerStateFqn)) {
                matched = true;
            } else if (isDistinctTypeMismatched(triggerStateFqn, machineStateFqn)) {
                mismatched = true;
            }
        }

        if (matched) {
            return DistinctTypeAffinity.FOR_MACHINE;
        }
        if (mismatched) {
            return DistinctTypeAffinity.AGAINST_MACHINE;
        }
        return DistinctTypeAffinity.UNKNOWN;
    }

    /** Concrete machine types only — unbound {@code E}/{@code S} and erasure placeholders → null. */
    private static String concreteTriggerType(String typeFqn) {
        return SharedServiceRoutingPolicy.isConcreteMachineType(typeFqn) ? typeFqn : null;
    }

    private static boolean hasConcretePolymorphicEvents(TriggerPoint trigger) {
        if (trigger == null || trigger.getPolymorphicEvents() == null) {
            return false;
        }
        for (String event : trigger.getPolymorphicEvents()) {
            if (event == null || event.isBlank() || event.startsWith("<SYMBOLIC:")) {
                continue;
            }
            if (event.startsWith("ENUM_SET:")) {
                continue;
            }
            return true;
        }
        return false;
    }

    private boolean matchesConfiguredTransitionEvent(
            TriggerPoint trigger,
            List<Transition> machineTransitions,
            CodebaseContext context) {
        if (trigger == null || machineTransitions == null || machineTransitions.isEmpty()) {
            return false;
        }
        EventMatchingEngine matchingEngine = new StrictFqnMatchingEngine(context);
        for (Transition transition : machineTransitions) {
            if (transition.getEvent() != null && matchingEngine.matches(transition.getEvent(), trigger)) {
                return true;
            }
        }
        return false;
    }

    private boolean isAmbiguousSharedGenericMatch(
            String triggerEventFqn,
            String triggerStateFqn,
            String machineStateFqn,
            String machineEventFqn) {
        boolean eventShared = triggerEventFqn != null
                && machineEventFqn != null
                && typesEquivalent(triggerEventFqn, machineEventFqn)
                && isAmbiguousSharedGenericType(triggerEventFqn);
        boolean stateShared = triggerStateFqn != null
                && machineStateFqn != null
                && typesEquivalent(triggerStateFqn, machineStateFqn)
                && isAmbiguousSharedGenericType(triggerStateFqn);
        return eventShared || stateShared;
    }

    private boolean isAmbiguousSharedGenericType(String typeFqn) {
        if (typeFqn == null || typeFqn.isBlank()) {
            return false;
        }
        return !SharedServiceRoutingPolicy.isConcreteMachineType(typeFqn);
    }

    private boolean isErasedOrOpaqueType(String typeFqn) {
        if (typeFqn == null) {
            return false;
        }
        String erased = eraseGenerics(typeFqn);
        return "java.lang.Object".equals(erased) || "Object".equals(erased)
                || "java.io.Serializable".equals(erased) || "Serializable".equals(erased)
                || (erased.length() == 1 && Character.isUpperCase(erased.charAt(0)));
    }

    private boolean isDistinctTypeMismatched(String type1, String type2) {
        if (isAmbiguousSharedGenericType(type1) || isAmbiguousSharedGenericType(type2)) {
            return false;
        }
        return isTypeMismatched(type1, type2);
    }

    private boolean isStringType(String typeFqn) {
        return MachineEnumCanonicalizer.isJavaStringType(typeFqn);
    }

    private Boolean resolveNamedBeanTarget(String beanName, String currentMachineName, CodebaseContext context) {
        return EnableStateMachineBeanRouting.matchesNamedBean(beanName, currentMachineName, context);
    }

    private boolean isAmbiguousSharedGenericTrigger(TriggerPoint trigger) {
        if (trigger == null) {
            return false;
        }
        return isAmbiguousSharedGenericType(trigger.getEventTypeFqn())
                || isAmbiguousSharedGenericType(trigger.getStateTypeFqn());
    }

    private int countStateMachines(CodebaseContext context) {
        if (context == null) {
            return 0;
        }
        Set<String> machines = new HashSet<>();
        for (TypeDeclaration td : context.findEntryPointClasses(
                java.util.List.of("EnableStateMachineFactory", "EnableStateMachine"))) {
            String fqn = context.getFqn(td);
            if (fqn != null) {
                machines.add(fqn);
            }
        }
        for (TypeDeclaration td : context.getTypeDeclarations()) {
            if (td == null || td.isInterface()) {
                continue;
            }
            if (context.extendsStateMachineConfigurerAdapter(td)) {
                String fqn = context.getFqn(td);
                if (fqn != null) {
                    machines.add(fqn);
                }
            }
        }
        if (!machines.isEmpty()) {
            return machines.size();
        }
        return context.findBeanMethodsReturning(
                java.util.Set.of("StateMachine", "StateMachineFactory", "StateMachineModelFactory")).size();
    }

    private String eraseGenerics(String type) {
        if (type == null) return null;
        int idx = type.indexOf('<');
        if (idx != -1) {
            return type.substring(0, idx);
        }
        return type;
    }

    private boolean typesEquivalent(String type1, String type2) {
        if (type1 == null || type2 == null) {
            return false;
        }
        String erased1 = normalizePrimitiveFqn(eraseGenerics(type1));
        String erased2 = normalizePrimitiveFqn(eraseGenerics(type2));
        return erased1.equals(erased2);
    }

    private String normalizePrimitiveFqn(String type) {
        if (type == null) {
            return null;
        }
        return switch (type) {
            case "java.lang.String" -> "String";
            case "java.lang.Object" -> "Object";
            case "java.io.Serializable" -> "Serializable";
            default -> type;
        };
    }

    private boolean isTypeMismatched(String type1, String type2) {
        if (type1 == null || type2 == null) return false;
        type1 = eraseGenerics(type1);
        type2 = eraseGenerics(type2);
        if (type1.length() == 1 || type2.length() == 1) return false;
        if (type1.equals("java.lang.String") || type1.equals("String") || type1.equals("java.lang.Object") || type1.equals("Object")) return false;
        if (type2.equals("java.lang.String") || type2.equals("String") || type2.equals("java.lang.Object") || type2.equals("Object")) return false;
        return !type1.equals(type2);
    }

    private boolean isSingleStateMachine(CodebaseContext context) {
        return countStateMachines(context) <= 1;
    }
}
