package click.kamil.springstatemachineexporter.analysis.enricher;

import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
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.resolver.DynamicTriggerExpressions;
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;
import click.kamil.springstatemachineexporter.analysis.service.ExternalTriggerPolicy;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import click.kamil.springstatemachineexporter.model.Event;
import click.kamil.springstatemachineexporter.model.Transition;

import java.util.ArrayList;
import java.util.List;

/**
 * Central rules for transition linking outcomes derived from existing trigger evidence only.
 */
public final class CallChainLinkPolicy {

    private CallChainLinkPolicy() {
    }

    /**
     * Resolves the machine event enum FQN from config AST, embedded analysis fields, or transition events.
     * Abstract/inherited configs often omit AST type args; transition {@code fullIdentifier} values still
     * carry the correct enum type for endpoint narrowing and linking.
     */
    public static String resolveMachineEventTypeFqn(
            String machineConfigName,
            String embeddedEventTypeFqn,
            StateMachineTypeResolver.MachineTypes machineTypes,
            List<Transition> machineTransitions,
            CodebaseContext context) {
        if (machineTypes != null && machineTypes.eventTypeFqn() != null && !machineTypes.eventTypeFqn().isBlank()) {
            return machineTypes.eventTypeFqn();
        }
        if (embeddedEventTypeFqn != null && !embeddedEventTypeFqn.isBlank()) {
            return embeddedEventTypeFqn;
        }
        if (context != null && machineConfigName != null) {
            String[] types = StateMachineTypeResolver.resolve(machineConfigName, context);
            if (types != null && types.length > 1 && types[1] != null && !types[1].isBlank()) {
                return types[1];
            }
        }
        return eventTypeFromTransitions(machineTransitions);
    }

    public static StateMachineTypeResolver.MachineTypes resolveEffectiveMachineTypes(
            String machineConfigName,
            String embeddedStateTypeFqn,
            String embeddedEventTypeFqn,
            StateMachineTypeResolver.MachineTypes machineTypes,
            List<Transition> machineTransitions,
            CodebaseContext context) {
        String eventTypeFqn = resolveMachineEventTypeFqn(
                machineConfigName, embeddedEventTypeFqn, machineTypes, machineTransitions, context);
        String stateTypeFqn = machineTypes != null && machineTypes.stateTypeFqn() != null
                ? machineTypes.stateTypeFqn()
                : embeddedStateTypeFqn;
        if ((stateTypeFqn == null || stateTypeFqn.isBlank()) && context != null && machineConfigName != null) {
            String[] types = StateMachineTypeResolver.resolve(machineConfigName, context);
            if (types != null && types.length > 0 && types[0] != null && !types[0].isBlank()) {
                stateTypeFqn = types[0];
            }
        }
        if ((stateTypeFqn == null || stateTypeFqn.isBlank()) && machineTransitions != null) {
            stateTypeFqn = stateTypeFromTransitions(machineTransitions);
        }
        return new StateMachineTypeResolver.MachineTypes(stateTypeFqn, eventTypeFqn);
    }

    private static String eventTypeFromTransitions(List<Transition> machineTransitions) {
        if (machineTransitions == null) {
            return null;
        }
        for (Transition transition : machineTransitions) {
            Event event = transition.getEvent();
            if (event == null) {
                continue;
            }
            String fullIdentifier = event.fullIdentifier() != null ? event.fullIdentifier() : event.rawName();
            if (fullIdentifier != null && fullIdentifier.contains(".")) {
                return fullIdentifier.substring(0, fullIdentifier.lastIndexOf('.'));
            }
        }
        return null;
    }

    private static String stateTypeFromTransitions(List<Transition> machineTransitions) {
        if (machineTransitions == null) {
            return null;
        }
        for (Transition transition : machineTransitions) {
            if (transition.getSourceStates() == null) {
                continue;
            }
            for (var state : transition.getSourceStates()) {
                String fullIdentifier = state.fullIdentifier() != null ? state.fullIdentifier() : state.rawName();
                if (fullIdentifier != null && fullIdentifier.contains(".")) {
                    return fullIdentifier.substring(0, fullIdentifier.lastIndexOf('.'));
                }
            }
        }
        return null;
    }

    public static boolean shouldFailClosedOnAmbiguousCallGraphWiden(TriggerPoint trigger) {
        return shouldFailClosedOnAmbiguousCallGraphWiden(trigger, null, null);
    }

    public static boolean shouldFailClosedOnAmbiguousCallGraphWiden(
            TriggerPoint trigger,
            String machineEventTypeFqn) {
        return shouldFailClosedOnAmbiguousCallGraphWiden(trigger, machineEventTypeFqn, null);
    }

    public static boolean shouldFailClosedOnAmbiguousCallGraphWiden(
            TriggerPoint trigger,
            String machineEventTypeFqn,
            CodebaseContext context) {
        if (trigger == null || trigger.isExternal()) {
            return false;
        }
        String eventTypeFqn = trigger.getEventTypeFqn();
        if (eventTypeFqn == null || eventTypeFqn.isBlank()) {
            eventTypeFqn = machineEventTypeFqn;
        }
        List<String> concrete = concretePolymorphicCandidates(
                trigger.getPolymorphicEvents(), eventTypeFqn, context);
        // Multiple concrete events on a dynamic/ENUM_SET trigger must not link every transition
        // (e.g. predicate-filtered PAY+SHIP, or an adjust endpoint sharing a wide dispatcher poly).
        // Do not require trigger.ambiguous — that flag is often cleared incorrectly upstream.
        if (concrete.size() <= 1) {
            return false;
        }
        String event = trigger.getEvent();
        if (event != null && event.startsWith("ENUM_SET:")) {
            return true;
        }
        return DynamicTriggerExpressions.isDynamic(event);
    }

    /**
     * True when call-graph evidence resolves to a single concrete machine enum event for this chain.
     * Dispatcher/rich-event paths should narrow to one constant per endpoint before linking.
     */
    public static boolean isEndpointNarrowPolyEvidence(
            TriggerPoint trigger,
            String machineEventTypeFqn,
            CodebaseContext context) {
        if (trigger == null) {
            return false;
        }
        String eventTypeFqn = trigger.getEventTypeFqn();
        if (eventTypeFqn == null || eventTypeFqn.isBlank()) {
            eventTypeFqn = machineEventTypeFqn;
        }
        List<String> concrete = concretePolymorphicCandidates(
                trigger.getPolymorphicEvents(), eventTypeFqn, context);
        return concrete.size() == 1;
    }

    public static boolean isEndpointNarrowPolyEvidence(TriggerPoint trigger) {
        return isEndpointNarrowPolyEvidence(trigger, null, null);
    }

    public static boolean isEndpointNarrowPolyEvidence(
            TriggerPoint trigger,
            String machineEventTypeFqn) {
        return isEndpointNarrowPolyEvidence(trigger, machineEventTypeFqn, null);
    }

    /**
     * True when every concrete polymorphic candidate belongs to the trigger's declared event type.
     * Used to allow machine-scoped symbolic expansion for generic dispatchers while rejecting
     * cross-package or unqualified widens.
     */
    static boolean isTrustedEnumPolymorphicWiden(TriggerPoint trigger) {
        return isTrustedEnumPolymorphicWiden(trigger, null, null);
    }

    static boolean isTrustedEnumPolymorphicWiden(TriggerPoint trigger, String machineEventTypeFqn) {
        return isTrustedEnumPolymorphicWiden(trigger, machineEventTypeFqn, null);
    }

    static boolean isTrustedEnumPolymorphicWiden(
            TriggerPoint trigger,
            String machineEventTypeFqn,
            CodebaseContext context) {
        if (trigger == null) {
            return false;
        }
        String eventTypeFqn = trigger.getEventTypeFqn();
        if (eventTypeFqn == null || eventTypeFqn.isBlank()) {
            eventTypeFqn = machineEventTypeFqn;
        }
        if (eventTypeFqn == null || eventTypeFqn.isBlank()) {
            return false;
        }
        List<String> poly = trigger.getPolymorphicEvents();
        List<String> concrete = concretePolymorphicCandidates(poly, eventTypeFqn, context);
        if (concrete.isEmpty()) {
            return false;
        }
        for (String pe : concrete) {
            if (!MachineEnumCanonicalizer.polymorphicEventMatchesMachineEventType(pe, eventTypeFqn, context)) {
                return false;
            }
        }
        return true;
    }

    static List<String> concretePolymorphicCandidates(
            List<String> poly,
            String eventTypeFqn,
            CodebaseContext context) {
        if (poly == null || poly.isEmpty()) {
            return List.of();
        }
        List<String> concrete = new ArrayList<>();
        for (String pe : poly) {
            if (pe == null || pe.startsWith("<SYMBOLIC:") || pe.startsWith("ENUM_SET:")) {
                continue;
            }
            String qualified = pe;
            if (!pe.contains(".")) {
                if (eventTypeFqn == null || eventTypeFqn.isBlank()) {
                    continue;
                }
                qualified = eventTypeFqn + "." + pe;
            } else if (eventTypeFqn != null && !eventTypeFqn.isBlank()) {
                qualified = MachineEnumCanonicalizer.canonicalizeLabel(pe, eventTypeFqn, context);
            }
            if (qualified != null && qualified.contains(".")) {
                concrete.add(qualified);
            }
        }
        return concrete;
    }

    public static LinkResolution resolveLinkResolution(
            TriggerPoint trigger,
            List<MatchedTransition> matched,
            boolean ambiguousSource) {
        return resolveLinkResolution(trigger, matched, ambiguousSource, null, null);
    }

    public static LinkResolution resolveLinkResolution(
            TriggerPoint trigger,
            List<MatchedTransition> matched,
            boolean ambiguousSource,
            String machineEventTypeFqn) {
        return resolveLinkResolution(trigger, matched, ambiguousSource, machineEventTypeFqn, null);
    }

    public static LinkResolution resolveLinkResolution(
            TriggerPoint trigger,
            List<MatchedTransition> matched,
            boolean ambiguousSource,
            String machineEventTypeFqn,
            CodebaseContext context) {
        // Prefer concrete matches over an external flag — dedicated endpoints can be
        // mis-tagged external when unrelated RequestParams appear in path bindings.
        if (matched != null && !matched.isEmpty()) {
            if (shouldFailClosedOnAmbiguousCallGraphWiden(trigger, machineEventTypeFqn, context)) {
                return LinkResolution.AMBIGUOUS_WIDEN;
            }
            return LinkResolution.RESOLVED;
        }
        if (trigger != null && trigger.isExternal()) {
            return LinkResolution.UNRESOLVED_EXTERNAL;
        }
        if (shouldFailClosedOnAmbiguousCallGraphWiden(trigger, machineEventTypeFqn, context)) {
            return LinkResolution.AMBIGUOUS_WIDEN;
        }
        if (ambiguousSource) {
            return LinkResolution.AMBIGUOUS_WIDEN;
        }
        return LinkResolution.NO_MATCH;
    }

    public static TriggerPoint applyRestExternalPolicy(TriggerPoint trigger, CallChain chain, CodebaseContext context) {
        if (trigger == null || chain.getEntryPoint() == null) {
            return trigger;
        }
        EntryPoint entryPoint = chain.getEntryPoint();
        String entryMethod = entryPoint.getClassName() + "." + entryPoint.getMethodName();
        // Pass null for event param name — trigger.getEvent() is an expression/FQN, not a REST param.
        boolean external = ExternalTriggerPolicy.isExternalFromSource(
                entryPoint, trigger, entryMethod, null, context);
        if (external == trigger.isExternal()) {
            return trigger;
        }
        return trigger.toBuilder().external(external).build();
    }
}
