package click.kamil.springstatemachineexporter.analysis.service;

import click.kamil.springstatemachineexporter.analysis.enricher.routing.SharedServiceRoutingPolicy;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.ParameterizedType;
import org.eclipse.jdt.core.dom.SimpleType;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.TypeParameter;

import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;

/**
 * Rebinds unbound {@code StateMachine<S,E>} type arguments on a trigger using the concrete
 * subclass parameterization present on the call chain.
 * <p>
 * Detection stamps types at the {@code sendEvent} site. On a shared base
 * {@code AbstractService.updateState}, those args are often unbound {@code S}/{@code E}.
 * After a chain exists (e.g. {@code OrderService extends AbstractService<OrderState, OrderEvent>}),
 * this binder substitutes the concrete types from that extends clause.
 */
public final class TriggerMachineTypeRebinder {

    private TriggerMachineTypeRebinder() {
    }

    public static TriggerPoint rebind(TriggerPoint trigger, List<String> methodChain, CodebaseContext context) {
        if (trigger == null || context == null) {
            return trigger;
        }
        boolean stateConcrete = SharedServiceRoutingPolicy.isConcreteMachineType(trigger.getStateTypeFqn());
        boolean eventConcrete = SharedServiceRoutingPolicy.isConcreteMachineType(trigger.getEventTypeFqn());
        if (stateConcrete && eventConcrete) {
            return trigger;
        }

        String ownerFqn = trigger.getClassName();
        LinkedHashSet<String> stateCandidates = new LinkedHashSet<>();
        LinkedHashSet<String> eventCandidates = new LinkedHashSet<>();

        if (ownerFqn != null && methodChain != null && !methodChain.isEmpty()) {
            TypeDeclaration ownerTd = context.getTypeDeclaration(ownerFqn);
            List<String> ownerTypeParams = typeParameterNames(ownerTd);
            int stateParamIndex = resolveStateParamIndex(trigger.getStateTypeFqn(), ownerTypeParams);
            int eventParamIndex = resolveEventParamIndex(trigger.getEventTypeFqn(), ownerTypeParams);

            TypeResolver typeResolver = new TypeResolver(context);
            for (String methodFqn : methodChain) {
                String chainClassFqn = classNameFromMethodFqn(methodFqn);
                if (chainClassFqn == null || chainClassFqn.equals(ownerFqn)) {
                    continue;
                }
                AbstractTypeDeclaration chainType = context.getAbstractTypeDeclaration(chainClassFqn);
                if (!(chainType instanceof TypeDeclaration chainTd)) {
                    continue;
                }
                collectFromSupertype(
                        chainTd.getSuperclassType(),
                        ownerFqn,
                        stateParamIndex,
                        eventParamIndex,
                        chainTd,
                        typeResolver,
                        stateCandidates,
                        eventCandidates);
                for (Object ifaceObj : chainTd.superInterfaceTypes()) {
                    if (ifaceObj instanceof Type ifaceType) {
                        collectFromSupertype(
                                ifaceType,
                                ownerFqn,
                                stateParamIndex,
                                eventParamIndex,
                                chainTd,
                                typeResolver,
                                stateCandidates,
                                eventCandidates);
                    }
                }
            }
        }

        String newState = stateConcrete
                ? trigger.getStateTypeFqn()
                : uniqueConcrete(stateCandidates);
        String newEvent = eventConcrete
                ? trigger.getEventTypeFqn()
                : uniqueConcrete(eventCandidates);

        if (!eventConcrete && newEvent == null) {
            newEvent = commonPolymorphicEventOwner(trigger.getPolymorphicEvents());
        }

        // Drop unresolved type-variable / erasure placeholders so routing treats them as absent.
        if (!stateConcrete && !SharedServiceRoutingPolicy.isConcreteMachineType(newState)) {
            newState = null;
        }
        if (!eventConcrete && !SharedServiceRoutingPolicy.isConcreteMachineType(newEvent)) {
            newEvent = null;
        }

        if (Objects.equals(newState, trigger.getStateTypeFqn())
                && Objects.equals(newEvent, trigger.getEventTypeFqn())) {
            return trigger;
        }
        return trigger.toBuilder()
                .stateTypeFqn(newState)
                .eventTypeFqn(newEvent)
                .build();
    }

    private static void collectFromSupertype(
            Type superType,
            String ownerFqn,
            int stateParamIndex,
            int eventParamIndex,
            TypeDeclaration chainTd,
            TypeResolver typeResolver,
            Set<String> stateCandidates,
            Set<String> eventCandidates) {
        if (!(superType instanceof ParameterizedType parameterized)) {
            return;
        }
        CompilationUnit cu = chainTd.getRoot() instanceof CompilationUnit root ? root : null;
        String baseFqn = typeResolver.resolveTypeToFqn(parameterized.getType(), cu);
        if (!ownerMatches(ownerFqn, baseFqn, parameterized.getType())) {
            return;
        }
        List<?> typeArgs = parameterized.typeArguments();
        if (stateParamIndex >= 0 && stateParamIndex < typeArgs.size()) {
            String resolved = typeResolver.resolveTypeToFqn((Type) typeArgs.get(stateParamIndex), cu);
            if (SharedServiceRoutingPolicy.isConcreteMachineType(resolved)) {
                stateCandidates.add(resolved);
            }
        }
        if (eventParamIndex >= 0 && eventParamIndex < typeArgs.size()) {
            String resolved = typeResolver.resolveTypeToFqn((Type) typeArgs.get(eventParamIndex), cu);
            if (SharedServiceRoutingPolicy.isConcreteMachineType(resolved)) {
                eventCandidates.add(resolved);
            }
        }
    }

    private static boolean ownerMatches(String ownerFqn, String resolvedBaseFqn, Type baseType) {
        if (ownerFqn == null) {
            return false;
        }
        if (ownerFqn.equals(resolvedBaseFqn)) {
            return true;
        }
        String simpleOwner = ownerFqn.contains(".")
                ? ownerFqn.substring(ownerFqn.lastIndexOf('.') + 1)
                : ownerFqn;
        if (resolvedBaseFqn != null
                && (resolvedBaseFqn.equals(simpleOwner) || resolvedBaseFqn.endsWith("." + simpleOwner))) {
            return true;
        }
        if (baseType instanceof SimpleType simpleType) {
            String name = simpleType.getName().getFullyQualifiedName();
            return simpleOwner.equals(name) || ownerFqn.endsWith("." + name);
        }
        return baseType != null && simpleOwner.equals(baseType.toString());
    }

    private static List<String> typeParameterNames(TypeDeclaration typeDeclaration) {
        if (typeDeclaration == null) {
            return List.of();
        }
        List<String> names = new ArrayList<>();
        for (Object paramObj : typeDeclaration.typeParameters()) {
            if (paramObj instanceof TypeParameter typeParameter) {
                names.add(typeParameter.getName().getIdentifier());
            }
        }
        return names;
    }

    /**
     * StateMachine&lt;S,E&gt; convention: first type param is state, second is event, unless the
     * trigger still carries the type-variable name (map by name).
     */
    private static int resolveStateParamIndex(String stateTypeFqn, List<String> ownerTypeParams) {
        if (stateTypeFqn != null && ownerTypeParams.contains(stateTypeFqn)) {
            return ownerTypeParams.indexOf(stateTypeFqn);
        }
        return ownerTypeParams.isEmpty() ? -1 : 0;
    }

    private static int resolveEventParamIndex(String eventTypeFqn, List<String> ownerTypeParams) {
        if (eventTypeFqn != null && ownerTypeParams.contains(eventTypeFqn)) {
            return ownerTypeParams.indexOf(eventTypeFqn);
        }
        return ownerTypeParams.size() >= 2 ? 1 : -1;
    }

    private static String uniqueConcrete(Set<String> candidates) {
        if (candidates == null || candidates.size() != 1) {
            return null;
        }
        return candidates.iterator().next();
    }

    private static String commonPolymorphicEventOwner(List<String> polymorphicEvents) {
        if (polymorphicEvents == null || polymorphicEvents.isEmpty()) {
            return null;
        }
        String owner = null;
        for (String event : polymorphicEvents) {
            if (event == null || event.isBlank() || event.startsWith("<SYMBOLIC:") || event.startsWith("ENUM_SET:")) {
                continue;
            }
            int lastDot = event.lastIndexOf('.');
            if (lastDot <= 0) {
                return null;
            }
            String candidateOwner = event.substring(0, lastDot);
            if (!SharedServiceRoutingPolicy.isConcreteMachineType(candidateOwner)) {
                return null;
            }
            if (owner == null) {
                owner = candidateOwner;
            } else if (!owner.equals(candidateOwner)) {
                return null;
            }
        }
        return owner;
    }

    private static String classNameFromMethodFqn(String methodFqn) {
        if (methodFqn == null || !methodFqn.contains(".")) {
            return null;
        }
        String clean = methodFqn;
        if (clean.contains("(")) {
            clean = clean.substring(0, clean.indexOf('('));
        }
        int lastDot = clean.lastIndexOf('.');
        if (lastDot <= 0) {
            return null;
        }
        return clean.substring(0, lastDot);
    }
}
