package click.kamil.springstatemachineexporter.analysis.enricher;

import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.LifecycleTriggerMarkers;
import click.kamil.springstatemachineexporter.analysis.model.LinkResolution;
import click.kamil.springstatemachineexporter.analysis.model.MatchedTransition;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
import click.kamil.springstatemachineexporter.analysis.service.EventCarrierPolicy;
import click.kamil.springstatemachineexporter.analysis.service.JsonExportContextFactory;
import click.kamil.springstatemachineexporter.analysis.service.TriggerMachineTypeRebinder;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import click.kamil.springstatemachineexporter.model.State;
import click.kamil.springstatemachineexporter.model.Transition;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import click.kamil.springstatemachineexporter.analysis.enricher.matching.EventMatchingEngine;
import click.kamil.springstatemachineexporter.analysis.enricher.matching.StrictFqnMatchingEngine;
import click.kamil.springstatemachineexporter.analysis.enricher.routing.BeanResolutionEngine;
import click.kamil.springstatemachineexporter.analysis.enricher.routing.HeuristicBeanResolutionEngine;
import click.kamil.springstatemachineexporter.analysis.enricher.routing.SharedServiceRoutingPolicy;
import click.kamil.springstatemachineexporter.analysis.resolver.BooleanConstraintEvaluator;
import click.kamil.springstatemachineexporter.analysis.resolver.DynamicTriggerExpressions;
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;

public class TransitionLinkerEnricher implements AnalysisEnricher {

    private final BeanResolutionEngine routingEngine = new HeuristicBeanResolutionEngine();

    @Override
    public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
        if (result.getMetadata() == null || result.getMetadata().getCallChains() == null) {
            return;
        }

        EventMatchingEngine matchingEngine = new StrictFqnMatchingEngine(context);

        List<CallChain> updatedChains = new ArrayList<>();
        List<Transition> stateMachineTransitions = result.getTransitions();
        StateMachineTypeResolver.MachineTypes machineTypes =
                JsonExportContextFactory.resolveMachineTypes(
                        result.getName(),
                        result.getStateTypeFqn(),
                        result.getEventTypeFqn(),
                        context);
        StateMachineTypeResolver.MachineTypes effectiveMachineTypes =
                CallChainLinkPolicy.resolveEffectiveMachineTypes(
                        result.getName(),
                        result.getStateTypeFqn(),
                        result.getEventTypeFqn(),
                        machineTypes,
                        stateMachineTransitions,
                        context);
        String effectiveEventTypeFqn = effectiveMachineTypes.eventTypeFqn();

        List<CallChain> workList = new ArrayList<>();
        for (CallChain chain : result.getMetadata().getCallChains()) {
            TriggerPoint tp = chain.getTriggerPoint();
            if (tp == null || tp.getEvent() == null) {
                workList.add(chain);
                continue;
            }

            tp = MachineEnumCanonicalizer.prepareCallChainTrigger(
                    tp,
                    effectiveMachineTypes,
                    context,
                    stateMachineTransitions,
                    chain.getPathBindings());
            tp = CallChainPolyNarrower.narrowForCallChain(
                    tp, chain, effectiveMachineTypes, result.getEventTypeFqn(), stateMachineTransitions, context);
            tp = MachineEnumCanonicalizer.canonicalizeTriggerLabelsForLinking(tp, effectiveMachineTypes, context);
            tp = TriggerMachineTypeRebinder.rebind(tp, chain.getMethodChain(), context);
            tp = CallChainLinkPolicy.applyRestExternalPolicy(tp, chain, context);
            tp = tp.toBuilder()
                    .eventTypeFqn(SharedServiceRoutingPolicy.concreteOrFallback(
                            tp.getEventTypeFqn(), effectiveEventTypeFqn))
                    .stateTypeFqn(SharedServiceRoutingPolicy.concreteOrFallback(
                            tp.getStateTypeFqn(), effectiveMachineTypes.stateTypeFqn()))
                    .build();

            workList.addAll(ExpandableEntryPolySplitter.maybeExpand(
                    chain, tp, effectiveEventTypeFqn, context));
        }

        for (CallChain chain : workList) {
            TriggerPoint tp = chain.getTriggerPoint();
            if (tp == null || tp.getEvent() == null) {
                updatedChains.add(chain);
                continue;
            }

            if (LifecycleTriggerMarkers.isLifecycle(tp.getEvent())) {
                updatedChains.add(chain);
                continue;
            }

            // Unbound event templates (REST /{event}) and unbound messaging payloads stay unresolved.
            // Resource placeholders like /{id}/pay must still link when the event is known.
            if (tp.isExternal()
                    && isUnresolvedExternalEndpoint(chain, tp)
                    && !hasConcreteEventEvidence(tp)) {
                updatedChains.add(chain.toBuilder()
                        .triggerPoint(tp)
                        .matchedTransitions(null)
                        .linkResolution(LinkResolution.UNRESOLVED_EXTERNAL)
                        .build());
                continue;
            }

            // Clear spurious external flags (unrelated PathVariable/RequestParam) once we can link.
            if (tp.isExternal()
                    && (!isUnresolvedExternalEndpoint(chain, tp) || hasConcreteEventEvidence(tp))) {
                tp = tp.toBuilder().external(false).build();
            }

            String triggerSource = tp.getSourceState() != null ? simplifySourceState(tp.getSourceState()) : null;
            if (CallChainLinkPolicy.shouldFailClosedOnAmbiguousCallGraphWiden(
                    tp,
                    effectiveEventTypeFqn,
                    context)) {
                updatedChains.add(chain.toBuilder()
                        .triggerPoint(tp)
                        .matchedTransitions(null)
                        .linkResolution(LinkResolution.AMBIGUOUS_WIDEN)
                        .build());
                continue;
            }

            boolean ambiguousSource = false;

            if (triggerSource == null) {
                Map<String, Set<String>> sourcesByEvent = new LinkedHashMap<>();
                Map<String, Set<String>> stateTypesByEvent = new LinkedHashMap<>();
                Map<String, String> canonicalSourceBySimplified = new LinkedHashMap<>();
                for (Transition t : stateMachineTransitions) {
                    if (t.getEvent() != null && matchingEngine.matches(t.getEvent(), tp)
                            && isRoutedToCorrectMachine(
                                    chain, result.getName(), context, stateMachineTransitions, tp,
                                        effectiveEventTypeFqn)
                            && isConstraintCompatible(tp.getConstraint(), result.getName())) {
                        String smEventForLink = canonicalEvent(t.getEvent());
                        for (State smSourceState : t.getSourceStates()) {
                            String smSourceForLink = canonicalState(smSourceState);
                            String smSource = simplifySourceState(smSourceForLink);
                            sourcesByEvent.computeIfAbsent(smEventForLink, ignored -> new LinkedHashSet<>())
                                    .add(smSource);
                            stateTypesByEvent.computeIfAbsent(smEventForLink, ignored -> new LinkedHashSet<>())
                                    .add(extractStateEnumType(smSourceForLink));
                            canonicalSourceBySimplified.putIfAbsent(smSource, smSourceForLink);
                        }
                    }
                }
                boolean crossStateTypeConflict = stateTypesByEvent.values().stream()
                        .anyMatch(types -> types.size() > 1);
                if (crossStateTypeConflict) {
                    ambiguousSource = true;
                } else if (sourcesByEvent.values().stream().anyMatch(sources -> sources.size() > 1)) {
                    // Same event from multiple sources within one state enum is normal — link all.
                    // Do not require endpoint-narrow poly evidence (that conflates "which event?"
                    // with "which place?").
                    ambiguousSource = false;
                } else {
                    Set<String> allDistinctSources = new LinkedHashSet<>();
                    sourcesByEvent.values().forEach(allDistinctSources::addAll);
                    if (allDistinctSources.size() == 1) {
                        String inferredSimplifiedSource = allDistinctSources.iterator().next();
                        String inferredCanonicalSource = canonicalSourceBySimplified.get(inferredSimplifiedSource);
                        tp = tp.toBuilder().sourceState(inferredCanonicalSource).build();
                        triggerSource = inferredSimplifiedSource;
                    }
                }
            }

            List<MatchedTransition> matched = new ArrayList<>();

            if (!ambiguousSource) {
            for (Transition t : stateMachineTransitions) {
                if (t.getEvent() != null && matchingEngine.matches(t.getEvent(), tp)) {
                    String smEventForLink = canonicalEvent(t.getEvent());
                    for (State smSourceState : t.getSourceStates()) {
                            String smSourceForLink = canonicalState(smSourceState);
                            String smSource = simplifySourceState(smSourceForLink);
                            if (triggerSource == null || triggerSource.equals(smSource)) {
                                if (t.getTargetStates() == null || t.getTargetStates().isEmpty()) {
                                    MatchedTransition mt = MatchedTransition.builder()
                                            .sourceState(smSourceForLink)
                                            .targetState(smSourceForLink)
                                            .event(smEventForLink)
                                            .build();
                                    if (isRoutedToCorrectMachine(
                                        chain, result.getName(), context, stateMachineTransitions, tp,
                                        effectiveEventTypeFqn)
                                        && isConstraintCompatible(tp.getConstraint(), result.getName())) {
                                        matched.add(mt);
                                    }
                                } else {
                                    for (State smTargetState : t.getTargetStates()) {
                                        String targetForLink = canonicalState(smTargetState);
                                        MatchedTransition mt = MatchedTransition.builder()
                                                .sourceState(smSourceForLink)
                                                .targetState(targetForLink)
                                                .event(smEventForLink)
                                                .build();
                                        if (isRoutedToCorrectMachine(
                                                chain, result.getName(), context, stateMachineTransitions, tp,
                                                effectiveEventTypeFqn)
                                                && isConstraintCompatible(tp.getConstraint(), result.getName())) {
                                            matched.add(mt);
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }

            if (ambiguousSource) {
                tp = tp.toBuilder().ambiguous(true).build();
            }

            LinkResolution linkResolution = CallChainLinkPolicy.resolveLinkResolution(
                    tp,
                    matched,
                    ambiguousSource,
                    effectiveEventTypeFqn,
                    context);

            if (!matched.isEmpty()) {
                CallChain newChain = chain.toBuilder()
                        .triggerPoint(tp)
                        .matchedTransitions(matched)
                        .linkResolution(linkResolution)
                        .build();
                updatedChains.add(newChain);
            } else {
                updatedChains.add(chain.toBuilder()
                        .triggerPoint(tp)
                        .matchedTransitions(null)
                        .linkResolution(linkResolution)
                        .build());
            }
        }

        List<TriggerPoint> scopedTriggers = MachineScopeFilter.filterTriggersForMachine(
                result.getMetadata().getTriggers(), result.getName(), context, stateMachineTransitions);
        List<CallChain> scopedChains = MachineScopeFilter.filterCallChainsForMachine(
                updatedChains, result.getName(), context, stateMachineTransitions);
        scopedChains = SemanticCallChainDedup.deduplicate(scopedChains);
        List<EntryPoint> scopedEntryPoints = EntryPointScopeResolver.scopeFromCallChains(
                scopedChains, result.getMetadata().getEntryPoints());

        CodebaseMetadata updatedMetadata = CodebaseMetadata.builder()
                .triggers(scopedTriggers)
                .entryPoints(scopedEntryPoints)
                .callChains(scopedChains)
                .properties(result.getMetadata().getProperties())
                .build();

        result.setMetadata(updatedMetadata);
    }

    
    private boolean isRoutedToCorrectMachine(
            CallChain chain,
            String currentMachineName,
            CodebaseContext context,
            List<Transition> machineTransitions,
            TriggerPoint triggerPoint,
            String machineEventTypeFqn) {
        if (triggerPoint != null
                && machineEventTypeFqn != null
                && CallChainLinkPolicy.isTrustedEnumPolymorphicWiden(triggerPoint, machineEventTypeFqn, context)) {
            return true;
        }
        return routingEngine.hasProvenMachineAffinity(chain, currentMachineName, context, machineTransitions);
    }

    private boolean isRoutedToCorrectMachine(
            CallChain chain, String currentMachineName, CodebaseContext context, List<Transition> machineTransitions) {
        return isRoutedToCorrectMachine(
                chain, currentMachineName, context, machineTransitions, chain.getTriggerPoint(), null);
    }

    private boolean isConstraintCompatible(String constraint, String machineName) {
        if (constraint == null || machineName == null) {
            return true;
        }
        return BooleanConstraintEvaluator.isCompatibleWithMachineDomain(
                constraint, MachineDomainKeys.extractMachineDomainKey(machineName));
    }

    private String extractStateEnumType(String stateIdentifier) {
        if (stateIdentifier == null || stateIdentifier.isBlank()) {
            return stateIdentifier;
        }
        int lastDot = stateIdentifier.lastIndexOf('.');
        if (lastDot <= 0) {
            // Bare constant (NEW) — treat as same unknown type within one machine analysis.
            return "";
        }
        return stateIdentifier.substring(0, lastDot);
    }

    private final java.util.Map<String, String> simplifySourceCache = new java.util.HashMap<>();

    private String simplifySourceState(String name) {
        if (name == null) return null;
        if (simplifySourceCache.containsKey(name)) return simplifySourceCache.get(name);

        // For source states we must preserve enum type context to avoid collisions between
        // different state enums that share a constant name (e.g. OrderState.NEW vs InvoiceState.NEW).
        // Use EnumFormat.fn-like suffix (Type.CONST) when possible.
        String simplified = name;
        int lastDot = name.lastIndexOf('.');
        if (lastDot > 0) {
            int prevDot = name.lastIndexOf('.', lastDot - 1);
            if (prevDot > 0) {
                simplified = name.substring(prevDot + 1);
            }
        }

        simplifySourceCache.put(name, simplified);
        return simplified;
    }

    private String canonicalState(State state) {
        if (state == null) {
            return null;
        }
        return state.fullIdentifier() != null ? state.fullIdentifier() : state.rawName();
    }

    private String canonicalEvent(click.kamil.springstatemachineexporter.model.Event event) {
        if (event == null) {
            return null;
        }
        return event.fullIdentifier() != null ? event.fullIdentifier() : event.rawName();
    }

    /**
     * True when the entry path still has an unbound event-carrying placeholder
     * ({@code {event}}, {@code {commandKey}}, …). Resource ids like {@code {id}} do not count.
     */
    private static boolean isUnresolvedExternalEndpoint(CallChain chain, TriggerPoint trigger) {
        if (chain == null) {
            return false;
        }
        if (isUnresolvedGenericRestEndpoint(chain.getEntryPoint())) {
            return true;
        }
        return isUnresolvedMessagingPayloadEndpoint(chain, trigger);
    }

    private static boolean isUnresolvedMessagingPayloadEndpoint(CallChain chain, TriggerPoint trigger) {
        EntryPoint entryPoint = chain.getEntryPoint();
        if (entryPoint == null || entryPoint.getType() == null || trigger == null) {
            return false;
        }
        boolean messaging = switch (entryPoint.getType()) {
            case JMS, KAFKA, RABBIT, SQS, SNS -> true;
            default -> false;
        };
        if (!messaging) {
            return false;
        }
        if (!DynamicTriggerExpressions.isDynamic(trigger.getEvent())) {
            return false;
        }
        // Expander variants carry concrete payload bindings (same channel as REST pathBindings).
        Map<String, String> bindings = chain.getPathBindings();
        return bindings == null || bindings.isEmpty();
    }

    private static boolean isUnresolvedGenericRestEndpoint(EntryPoint entryPoint) {
        if (entryPoint == null) {
            return false;
        }
        if (hasUnboundEventPlaceholder(entryPoint.getName())) {
            return true;
        }
        if (entryPoint.getMetadata() != null) {
            Object path = entryPoint.getMetadata().get("path");
            if (path instanceof String pathValue && hasUnboundEventPlaceholder(pathValue)) {
                return true;
            }
        }
        return false;
    }

    private static boolean hasUnboundEventPlaceholder(String pathOrName) {
        if (pathOrName == null || pathOrName.isBlank() || !pathOrName.contains("{")) {
            return false;
        }
        int start = 0;
        while (true) {
            int open = pathOrName.indexOf('{', start);
            if (open < 0) {
                return false;
            }
            int close = pathOrName.indexOf('}', open + 1);
            if (close < 0) {
                return false;
            }
            String placeholder = pathOrName.substring(open + 1, close).trim();
            if (isEventLikePlaceholder(placeholder)) {
                return true;
            }
            start = close + 1;
        }
    }

    private static boolean isEventLikePlaceholder(String placeholder) {
        if (placeholder == null || placeholder.isBlank()) {
            return false;
        }
        String lower = placeholder.toLowerCase();
        // Strip matrix/regex extras: event:.+ or event:.*
        int colon = lower.indexOf(':');
        if (colon > 0) {
            lower = lower.substring(0, colon);
        }
        return EventCarrierPolicy.isCarrierParamName(lower)
                || EventCarrierPolicy.isMachineDiscriminatorName(lower);
    }

    private static boolean hasConcreteEventEvidence(TriggerPoint trigger) {
        if (trigger == null) {
            return false;
        }
        // Only a resolved enum constant on the trigger event itself bypasses unbound {event}
        // templates. Polymorphic widens / single-transition inference must not count — otherwise
        // every generic dispatcher template becomes RESOLVED against a one-transition machine.
        return MachineEnumCanonicalizer.classifyTriggerEvent(trigger.getEvent())
                == MachineEnumCanonicalizer.TriggerEventKind.CANONICAL_ENUM;
    }
}