package click.kamil.springstatemachineexporter.analysis.resolver;

import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import click.kamil.springstatemachineexporter.model.Event;
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.Objects;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * Canonicalizes enum-backed state/event identifiers for analysis using the owning machine config's
 * {@code StateMachineConfigurerAdapter<State, Event>} type parameters.
 *
 * <p>Display formatting ({@code --event}/{@code --state}) is separate; this fixes internal
 * {@code fullIdentifier} values used for matching and traceability.
 */
public final class MachineEnumCanonicalizer {

    private MachineEnumCanonicalizer() {
    }

    public static void canonicalizeTransitions(
            List<Transition> transitions,
            StateMachineTypeResolver.MachineTypes machineTypes) {
        canonicalizeTransitions(transitions, machineTypes, null);
    }

    public static void canonicalizeTransitions(
            List<Transition> transitions,
            StateMachineTypeResolver.MachineTypes machineTypes,
            CodebaseContext context) {
        if (transitions == null || machineTypes == null) {
            return;
        }
        for (Transition transition : transitions) {
            if (transition.getEvent() != null) {
                transition.setEvent(canonicalizeEvent(transition.getEvent(), machineTypes.eventTypeFqn(), context));
            }
            if (transition.getSourceStates() != null) {
                transition.setSourceStates(canonicalizeStates(transition.getSourceStates(), machineTypes.stateTypeFqn(), context));
            }
            if (transition.getTargetStates() != null) {
                transition.setTargetStates(canonicalizeStates(transition.getTargetStates(), machineTypes.stateTypeFqn(), context));
            }
        }
    }

    public static Set<String> canonicalizeStateLabels(Set<String> labels, String stateTypeFqn) {
        return canonicalizeStateLabels(labels, stateTypeFqn, null);
    }

    public static Set<String> canonicalizeStateLabels(Set<String> labels, String stateTypeFqn, CodebaseContext context) {
        if (labels == null || labels.isEmpty()) {
            return labels;
        }
        return labels.stream()
                .map(label -> canonicalizeLabel(label, stateTypeFqn, context))
                .collect(Collectors.toCollection(LinkedHashSet::new));
    }

    public static Set<State> canonicalizeStates(Set<State> states, String stateTypeFqn) {
        return canonicalizeStates(states, stateTypeFqn, null);
    }

    public static Set<State> canonicalizeStates(Set<State> states, String stateTypeFqn, CodebaseContext context) {
        if (states == null || states.isEmpty()) {
            return states;
        }
        LinkedHashMap<String, State> byFullIdentifier = new LinkedHashMap<>();
        for (State state : states) {
            State canonical = canonicalizeState(state, stateTypeFqn, context);
            byFullIdentifier.putIfAbsent(canonical.fullIdentifier(), canonical);
        }
        return new LinkedHashSet<>(byFullIdentifier.values());
    }

    public static TriggerPoint canonicalizeTriggerPoint(
            TriggerPoint trigger,
            StateMachineTypeResolver.MachineTypes machineTypes) {
        return canonicalizeTriggerPoint(trigger, machineTypes, null);
    }

    public static TriggerPoint canonicalizeTriggerPoint(
            TriggerPoint trigger,
            StateMachineTypeResolver.MachineTypes machineTypes,
            CodebaseContext context) {
        if (trigger == null) {
            return null;
        }
        String eventTypeFqn = preferFullTypeFqn(
                trigger.getEventTypeFqn(),
                machineTypes != null ? machineTypes.eventTypeFqn() : null);
        String stateTypeFqn = preferFullTypeFqn(
                trigger.getStateTypeFqn(),
                machineTypes != null ? machineTypes.stateTypeFqn() : null);

        List<String> polymorphicEvents = trigger.getPolymorphicEvents() == null ? null
                : trigger.getPolymorphicEvents().stream()
                        .map(event -> canonicalizeLabel(event, eventTypeFqn, context))
                        .collect(Collectors.toList());

        return trigger.toBuilder()
                .eventTypeFqn(eventTypeFqn)
                .stateTypeFqn(stateTypeFqn)
                .event(canonicalizeLabel(trigger.getEvent(), eventTypeFqn, context))
                .sourceState(canonicalizeLabel(trigger.getSourceState(), stateTypeFqn, context))
                .polymorphicEvents(polymorphicEvents)
                .build();
    }

    /**
     * Canonicalizes trigger labels for linking using machine types, but preserves the trigger's own
     * {@code eventTypeFqn}/{@code stateTypeFqn} so routing evidence (e.g. shared-infrastructure
     * detection) is not overwritten before machine affinity is resolved.
     */
    public static TriggerPoint canonicalizeTriggerLabelsForLinking(
            TriggerPoint trigger,
            StateMachineTypeResolver.MachineTypes machineTypes,
            CodebaseContext context) {
        if (trigger == null) {
            return null;
        }
        String eventTypeFqnForLabels = preferFullTypeFqn(
                trigger.getEventTypeFqn(),
                machineTypes != null ? machineTypes.eventTypeFqn() : null);
        String stateTypeFqnForLabels = preferFullTypeFqn(
                trigger.getStateTypeFqn(),
                machineTypes != null ? machineTypes.stateTypeFqn() : null);

        List<String> polymorphicEvents = trigger.getPolymorphicEvents() == null ? null
                : trigger.getPolymorphicEvents().stream()
                        .map(event -> canonicalizeLabel(event, eventTypeFqnForLabels, context))
                        .collect(Collectors.toList());

        return trigger.toBuilder()
                .event(canonicalizeLabel(trigger.getEvent(), eventTypeFqnForLabels, context))
                .sourceState(canonicalizeLabel(trigger.getSourceState(), stateTypeFqnForLabels, context))
                .polymorphicEvents(polymorphicEvents)
                .build();
    }

    /**
     * Expands {@code <SYMBOLIC: EnumType.*>} placeholders into concrete package-canonical enum
     * constants for the owning machine's event type. Non-matching symbolic entries are dropped.
     */
    public static List<String> expandSymbolicPolymorphicEvents(
            List<String> polymorphicEvents,
            String machineEventTypeFqn,
            CodebaseContext context) {
        return expandSymbolicPolymorphicEvents(polymorphicEvents, machineEventTypeFqn, context, null);
    }

    public static List<String> expandSymbolicPolymorphicEvents(
            List<String> polymorphicEvents,
            String machineEventTypeFqn,
            CodebaseContext context,
            List<Transition> machineTransitions) {
        if (polymorphicEvents == null || polymorphicEvents.isEmpty()) {
            return polymorphicEvents;
        }
        if (machineEventTypeFqn == null || machineEventTypeFqn.isBlank()) {
            return polymorphicEvents;
        }

        List<String> transitionEvents = polymorphicEventsFromTransitions(machineTransitions, machineEventTypeFqn, context);

        List<String> expanded = new ArrayList<>();
        for (String pe : polymorphicEvents) {
            if (pe == null) {
                continue;
            }
            if (pe.startsWith("<SYMBOLIC: ") && pe.endsWith(".*>")) {
                String symbolicType = pe.substring("<SYMBOLIC: ".length(), pe.length() - 3).trim();
                if (context != null && context.isAmbiguousSimpleName(symbolicType)) {
                    continue;
                }
                if (!enumTypesMatch(machineEventTypeFqn, symbolicType, context)) {
                    continue;
                }
                if (transitionEvents.size() == 1) {
                    expanded.addAll(transitionEvents);
                    continue;
                }
                if (!transitionEvents.isEmpty()) {
                    // Multiple configured transitions: do not widen symbolic placeholders to all events.
                    continue;
                }
                if (context != null) {
                    List<String> enumValues = context.getEnumValues(stripGenerics(machineEventTypeFqn));
                    if (enumValues != null) {
                        for (String value : enumValues) {
                            String parsed = value;
                            if (parsed.contains(".")) {
                                parsed = parsed.substring(parsed.lastIndexOf('.') + 1);
                            }
                            expanded.add(stripGenerics(machineEventTypeFqn) + "." + parsed);
                        }
                    }
                }
                continue;
            }
            expanded.add(pe);
        }
        return expanded;
    }

    public static TriggerPoint canonicalizeAndExpandTriggerPoint(
            TriggerPoint trigger,
            StateMachineTypeResolver.MachineTypes machineTypes,
            CodebaseContext context) {
        TriggerPoint canonical = canonicalizeTriggerPoint(trigger, machineTypes, context);
        if (canonical == null || machineTypes == null || machineTypes.eventTypeFqn() == null) {
            return canonical;
        }
        List<String> expanded = expandSymbolicPolymorphicEvents(
                canonical.getPolymorphicEvents(), machineTypes.eventTypeFqn(), context);
        List<String> narrowed = narrowExpandedPolymorphicEvents(
                expanded,
                canonical.getConstraint(),
                machineTypes.eventTypeFqn(),
                null,
                context);
        if (Objects.equals(narrowed, canonical.getPolymorphicEvents())) {
            return canonical;
        }
        return canonical.toBuilder().polymorphicEvents(narrowed).build();
    }

    /**
     * Expands symbolic placeholders and applies predicate/transition narrowing when possible.
     */
    public static List<String> narrowExpandedPolymorphicEvents(
            List<String> polymorphicEvents,
            String constraint,
            String eventTypeFqn,
            List<Transition> machineTransitions,
            CodebaseContext context) {
        if (polymorphicEvents == null || polymorphicEvents.isEmpty()) {
            return polymorphicEvents;
        }
        return narrowPolymorphicCandidates(
                polymorphicEvents, constraint, eventTypeFqn, machineTransitions, context, null);
    }

    /**
     * When a call-chain trigger uses a dynamic fire-site expression ({@code valueOf}, variable, getter)
     * but has no concrete {@code polymorphicEvents}, infer all machine enum events so transition linking
     * can proceed. Used only for {@code callChains[].triggerPoint}, not {@code metadata.triggers}.
     */
    public static TriggerPoint ensureCallChainPolymorphicEvents(
            TriggerPoint trigger,
            StateMachineTypeResolver.MachineTypes machineTypes,
            CodebaseContext context) {
        return ensureCallChainPolymorphicEvents(trigger, machineTypes, context, null, false, null);
    }

    public static TriggerPoint ensureCallChainPolymorphicEvents(
            TriggerPoint trigger,
            StateMachineTypeResolver.MachineTypes machineTypes,
            CodebaseContext context,
            List<Transition> machineTransitions) {
        return ensureCallChainPolymorphicEvents(trigger, machineTypes, context, machineTransitions, false, null);
    }

    public static TriggerPoint ensureCallChainPolymorphicEvents(
            TriggerPoint trigger,
            StateMachineTypeResolver.MachineTypes machineTypes,
            CodebaseContext context,
            List<Transition> machineTransitions,
            boolean skipCanonicalization) {
        return ensureCallChainPolymorphicEvents(
                trigger, machineTypes, context, machineTransitions, skipCanonicalization, null);
    }

    /**
     * Shared call-chain trigger prep: bound {@code valueOf} from constraint/pathBindings, then
     * polymorphic ensure/widen. Used by linker, canonicalization, and finalizer so all flows share
     * the same pathBindings-aware channel.
     */
    public static TriggerPoint prepareCallChainTrigger(
            TriggerPoint trigger,
            StateMachineTypeResolver.MachineTypes machineTypes,
            CodebaseContext context,
            List<Transition> machineTransitions,
            Map<String, String> pathBindings) {
        TriggerPoint expanded = expandBoundValueOfFromConstraints(
                trigger, machineTypes, context, pathBindings);
        return ensureCallChainPolymorphicEvents(
                expanded, machineTypes, context, machineTransitions, true, pathBindings);
    }

    public static TriggerPoint ensureCallChainPolymorphicEvents(
            TriggerPoint trigger,
            StateMachineTypeResolver.MachineTypes machineTypes,
            CodebaseContext context,
            List<Transition> machineTransitions,
            boolean skipCanonicalization,
            Map<String, String> pathBindings) {
        TriggerPoint expanded = skipCanonicalization
                ? expandSymbolicOnly(trigger, machineTypes, context, machineTransitions)
                : canonicalizeAndExpandTriggerPoint(trigger, machineTypes, context);
        if (expanded == null || machineTypes == null || machineTypes.eventTypeFqn() == null) {
            return expanded;
        }
        List<String> postExpand = expanded.getPolymorphicEvents();
        if (postExpand != null && postExpand.stream().anyMatch(pe -> pe != null && pe.startsWith("<SYMBOLIC:"))) {
            postExpand = expandSymbolicPolymorphicEvents(
                    postExpand, machineTypes.eventTypeFqn(), context, machineTransitions);
            postExpand = narrowExpandedPolymorphicEvents(
                    postExpand,
                    expanded.getConstraint(),
                    machineTypes.eventTypeFqn(),
                    machineTransitions,
                    context);
            if (!Objects.equals(postExpand, expanded.getPolymorphicEvents())) {
                expanded = expanded.toBuilder().polymorphicEvents(postExpand).build();
            }
        }
        if (hasConcretePolymorphicEvents(expanded.getPolymorphicEvents())) {
            List<String> narrowed = narrowPolymorphicCandidates(
                    expanded.getPolymorphicEvents(),
                    expanded.getConstraint(),
                    machineTypes.eventTypeFqn(),
                    machineTransitions,
                    context,
                    expanded);
            if (!narrowed.equals(expanded.getPolymorphicEvents())) {
                return expanded.toBuilder()
                        .polymorphicEvents(narrowed)
                        .ambiguous(narrowed.size() > 1)
                        .build();
            }
            if (narrowed.size() > 1 && !expanded.isAmbiguous()) {
                return expanded.toBuilder().ambiguous(true).build();
            }
            return expanded;
        }
        if (expanded.getPolymorphicEvents() != null
                && !expanded.getPolymorphicEvents().isEmpty()
                && !isUnboundValueOfWidenCandidate(expanded, pathBindings)) {
            List<String> narrowed = narrowPolymorphicCandidates(
                    expanded.getPolymorphicEvents(),
                    expanded.getConstraint(),
                    machineTypes.eventTypeFqn(),
                    machineTransitions,
                    context,
                    expanded);
            if (!narrowed.equals(expanded.getPolymorphicEvents())) {
                return expanded.toBuilder()
                        .polymorphicEvents(narrowed)
                        .ambiguous(narrowed.size() > 1)
                        .build();
            }
            if (narrowed.size() > 1 && !expanded.isAmbiguous()) {
                return expanded.toBuilder().ambiguous(true).build();
            }
            return expanded;
        }
        if (shouldInferPolymorphicEvents(expanded, machineTransitions, context)) {
            List<String> machineEvents = inferPolymorphicCandidates(
                    expanded.getConstraint(),
                    machineTypes.eventTypeFqn(),
                    machineTransitions,
                    context,
                    expanded);
            if (!machineEvents.isEmpty()) {
                return expanded.toBuilder()
                        .polymorphicEvents(machineEvents)
                        .ambiguous(machineEvents.size() > 1)
                        .build();
            }
            // Predicate-driven inference failed closed — do not fall through to unbound valueOf widen.
            if (EnumMemberPredicateEvaluator.hasEnumMemberPredicates(expanded.getConstraint())) {
                return expanded;
            }
        }
        if (isUnboundValueOfWidenCandidate(expanded, pathBindings)) {
            List<String> widened = widenUnboundValueOfEvents(
                    machineTypes.eventTypeFqn(), machineTransitions, context);
            if (!widened.isEmpty()) {
                return expanded.toBuilder()
                        .polymorphicEvents(widened)
                        .ambiguous(widened.size() > 1)
                        .build();
            }
        }
        return expanded;
    }

    private static TriggerPoint expandSymbolicOnly(
            TriggerPoint trigger,
            StateMachineTypeResolver.MachineTypes machineTypes,
            CodebaseContext context,
            List<Transition> machineTransitions) {
        if (trigger == null || machineTypes == null || machineTypes.eventTypeFqn() == null) {
            return trigger;
        }
        List<String> expanded = expandSymbolicPolymorphicEvents(
                trigger.getPolymorphicEvents(), machineTypes.eventTypeFqn(), context, machineTransitions);
        if (java.util.Objects.equals(expanded, trigger.getPolymorphicEvents())) {
            return trigger;
        }
        return trigger.toBuilder().polymorphicEvents(expanded).build();
    }

    private static boolean shouldInferPolymorphicEvents(
            TriggerPoint trigger,
            List<Transition> machineTransitions,
            CodebaseContext context) {
        if (trigger == null || machineTransitions == null || machineTransitions.isEmpty()) {
            return false;
        }
        if (hasOnlySymbolicPolymorphicEvents(trigger.getPolymorphicEvents())) {
            if (context != null && hasAmbiguousSymbolicPolymorphicType(trigger.getPolymorphicEvents(), context)) {
                return false;
            }
            return true;
        }
        List<String> polyEvents = trigger.getPolymorphicEvents();
        if (polyEvents != null && !polyEvents.isEmpty()) {
            return false;
        }
        if (EnumMemberPredicateEvaluator.hasEnumMemberPredicates(trigger.getConstraint())) {
            return true;
        }
        if (trigger.isExternal()) {
            return true;
        }
        if (trigger.isAmbiguous()) {
            return false;
        }
        return classifyTriggerEvent(trigger.getEvent()) == TriggerEventKind.DYNAMIC_EXPRESSION;
    }

    private static boolean hasAmbiguousSymbolicPolymorphicType(
            List<String> polymorphicEvents,
            CodebaseContext context) {
        if (polymorphicEvents == null) {
            return false;
        }
        for (String pe : polymorphicEvents) {
            if (pe != null && pe.startsWith("<SYMBOLIC: ") && pe.endsWith(".*>")) {
                String symbolicType = pe.substring("<SYMBOLIC: ".length(), pe.length() - 3).trim();
                if (context.isAmbiguousSimpleName(symbolicType)) {
                    return true;
                }
            }
        }
        return false;
    }

    private static boolean hasOnlySymbolicPolymorphicEvents(List<String> polymorphicEvents) {
        if (polymorphicEvents == null || polymorphicEvents.isEmpty()) {
            return false;
        }
        boolean hasSymbolic = false;
        for (String pe : polymorphicEvents) {
            if (pe == null) {
                continue;
            }
            if (pe.startsWith("<SYMBOLIC:")) {
                hasSymbolic = true;
                continue;
            }
            if (classifyTriggerEvent(pe) == TriggerEventKind.CANONICAL_ENUM) {
                return false;
            }
        }
        return hasSymbolic;
    }

    private static List<String> inferPolymorphicCandidates(
            String constraint,
            String eventTypeFqn,
            List<Transition> machineTransitions,
            CodebaseContext context,
            TriggerPoint trigger) {
        List<String> transitionEvents = polymorphicEventsFromTransitions(machineTransitions, eventTypeFqn, context);
        if (transitionEvents.isEmpty()) {
            return List.of();
        }
        if (EnumMemberPredicateEvaluator.hasEnumMemberPredicates(constraint)) {
            List<String> filtered = EnumMemberPredicateEvaluator.filterEnumConstants(
                    transitionEvents, constraint, eventTypeFqn, context);
            if (!filtered.isEmpty()) {
                return filtered;
            }
            if (trigger != null) {
                List<String> richFiltered = RichEventPredicateEvaluator.filterEnumConstants(
                        transitionEvents, constraint, eventTypeFqn, trigger, context);
                if (!richFiltered.isEmpty()) {
                    return richFiltered;
                }
            }
            return List.of();
        }
        return transitionEvents.size() == 1 ? transitionEvents : List.of();
    }

    private static List<String> narrowPolymorphicCandidates(
            List<String> current,
            String constraint,
            String eventTypeFqn,
            List<Transition> machineTransitions,
            CodebaseContext context) {
        return narrowPolymorphicCandidates(
                current, constraint, eventTypeFqn, machineTransitions, context, null);
    }

    private static List<String> narrowPolymorphicCandidates(
            List<String> current,
            String constraint,
            String eventTypeFqn,
            List<Transition> machineTransitions,
            CodebaseContext context,
            TriggerPoint trigger) {
        List<String> transitionEvents = polymorphicEventsFromTransitions(machineTransitions, eventTypeFqn, context);
        List<String> result = current == null ? List.of() : new ArrayList<>(current);

        if (EnumMemberPredicateEvaluator.hasEnumMemberPredicates(constraint)) {
            List<String> filtered = EnumMemberPredicateEvaluator.filterEnumConstants(
                    result, constraint, eventTypeFqn, context);
            if (!filtered.isEmpty()) {
                result = filtered;
            } else if (trigger != null) {
                List<String> source = !transitionEvents.isEmpty() ? transitionEvents : result;
                List<String> richFiltered = RichEventPredicateEvaluator.filterEnumConstants(
                        source, constraint, eventTypeFqn, trigger, context);
                if (!richFiltered.isEmpty()) {
                    result = richFiltered;
                } else if (!transitionEvents.isEmpty()) {
                    filtered = EnumMemberPredicateEvaluator.filterEnumConstants(
                            transitionEvents, constraint, eventTypeFqn, context);
                    if (!filtered.isEmpty()) {
                        result = filtered;
                    } else {
                        result = List.of();
                    }
                } else {
                    result = List.of();
                }
            } else if (!transitionEvents.isEmpty()) {
                filtered = EnumMemberPredicateEvaluator.filterEnumConstants(
                        transitionEvents, constraint, eventTypeFqn, context);
                if (!filtered.isEmpty()) {
                    result = filtered;
                } else {
                    result = List.of();
                }
            } else {
                result = List.of();
            }
        }

        if (!transitionEvents.isEmpty()) {
            result = capToConfiguredTransitionEvents(result, transitionEvents, eventTypeFqn, context);
        }
        return result;
    }

    /**
     * Fail-closed ceiling: polymorphic candidates must never exceed events configured on the machine.
     */
    static List<String> capToConfiguredTransitionEvents(
            List<String> candidates,
            List<String> transitionEvents,
            String eventTypeFqn) {
        return capToConfiguredTransitionEvents(candidates, transitionEvents, eventTypeFqn, null);
    }

    static List<String> capToConfiguredTransitionEvents(
            List<String> candidates,
            List<String> transitionEvents,
            String eventTypeFqn,
            CodebaseContext context) {
        if (transitionEvents == null || transitionEvents.isEmpty()) {
            return candidates == null ? List.of() : candidates;
        }
        if (candidates == null || candidates.isEmpty()) {
            return List.of();
        }
        List<String> capped = new ArrayList<>();
        for (String candidate : candidates) {
            if (candidate == null) {
                continue;
            }
            if (transitionEvents.contains(candidate)) {
                capped.add(candidate);
                continue;
            }
            String candidateType = enumTypeFromRef(candidate);
            String constant = constantName(candidate);
            for (String transitionEvent : transitionEvents) {
                if (!constantName(transitionEvent).equals(constant)) {
                    continue;
                }
                String transitionType = enumTypeFromRef(transitionEvent);
                if (candidate.contains(".") && transitionEvent.contains(".")) {
                    if (candidateType != null && transitionType != null
                            && enumTypesMatch(candidateType, transitionType, context)) {
                        capped.add(transitionEvent);
                        break;
                    }
                } else if (eventTypeFqn != null && transitionType != null
                        && enumTypesMatch(eventTypeFqn, transitionType, context)) {
                    capped.add(transitionEvent);
                    break;
                }
            }
        }
        return capped;
    }

    public static List<String> polymorphicEventsFromTransitions(
            List<Transition> machineTransitions,
            String eventTypeFqn) {
        return polymorphicEventsFromTransitions(machineTransitions, eventTypeFqn, null);
    }

    public static List<String> polymorphicEventsFromTransitions(
            List<Transition> machineTransitions,
            String eventTypeFqn,
            CodebaseContext context) {
        if (machineTransitions == null || machineTransitions.isEmpty() || eventTypeFqn == null) {
            return List.of();
        }
        List<String> events = new ArrayList<>();
        for (Transition transition : machineTransitions) {
            if (transition.getEvent() == null) {
                continue;
            }
            String identifier = transition.getEvent().fullIdentifier() != null
                    ? transition.getEvent().fullIdentifier()
                    : transition.getEvent().rawName();
            if (identifier == null || identifier.isBlank()) {
                continue;
            }
            String canonical = canonicalizeLabel(identifier, eventTypeFqn, context);
            if (classifyTriggerEvent(canonical) == TriggerEventKind.CANONICAL_ENUM
                    && !events.contains(canonical)) {
                events.add(canonical);
            }
        }
        return events;
    }

    private static boolean hasConcretePolymorphicEvents(List<String> polymorphicEvents) {
        return polymorphicEvents != null
                && polymorphicEvents.stream()
                        .anyMatch(pe -> classifyTriggerEvent(pe) == TriggerEventKind.CANONICAL_ENUM);
    }

    public static String qualifyEventIdentifier(String event, String eventTypeFqn) {
        return qualifyEventIdentifier(event, eventTypeFqn, null);
    }

    public static String qualifyEventIdentifier(String event, String eventTypeFqn, CodebaseContext context) {
        if (event == null || eventTypeFqn == null || event.isEmpty()) {
            return event;
        }
        if (isDynamicTriggerExpression(event)) {
            return denormalizeCorruptedDynamicEvent(event);
        }
        if (event.startsWith("<SYMBOLIC: ")) {
            return event;
        }
        if (isWildcardVariable(event)) {
            return event;
        }
        if (Character.isLowerCase(event.charAt(0))) {
            return event;
        }
        if (!isEnumConstantCandidate(event)) {
            return event;
        }
        if (isStringOrPrimitiveType(eventTypeFqn)) {
            return event;
        }
        if (isMachineEnumReference(event, eventTypeFqn, context)) {
            return canonicalizeLabel(event, stripGenerics(eventTypeFqn), context);
        }
        return event;
    }

    private static String preferFullTypeFqn(String primary, String fallback) {
        if (primary != null && primary.contains(".")) {
            return stripGenerics(primary);
        }
        if (fallback != null && fallback.contains(".")) {
            return stripGenerics(fallback);
        }
        return primary != null ? primary : fallback;
    }

    private static String stripGenerics(String typeFqn) {
        int idx = typeFqn.indexOf('<');
        return idx >= 0 ? typeFqn.substring(0, idx) : typeFqn;
    }

    public static boolean isStringOrPrimitiveType(String typeFqn) {
        if (typeFqn == null) {
            return false;
        }
        return typeFqn.equals("String") || typeFqn.equals("java.lang.String")
                || typeFqn.equals("int") || typeFqn.equals("long") || typeFqn.equals("char")
                || typeFqn.equals("java.lang.Object") || typeFqn.equals("Object");
    }

    /**
     * String / primitive event-type markers used by strict matching. Unlike
     * {@link #isStringOrPrimitiveType}, does <em>not</em> treat {@code Object} as string-typed —
     * that would let bare constants match against Object-erased triggers too broadly.
     */
    public static boolean isStringOrPrimitiveEventType(String typeFqn) {
        if (typeFqn == null) {
            return false;
        }
        return typeFqn.equals("String") || typeFqn.equals("java.lang.String")
                || typeFqn.equals("int") || typeFqn.equals("long") || typeFqn.equals("char");
    }

    /** True for {@code String} / {@code java.lang.String} (optionally with generics erased). */
    public static boolean isJavaStringType(String typeFqn) {
        if (typeFqn == null || typeFqn.isBlank()) {
            return false;
        }
        String erased = typeFqn;
        int generic = erased.indexOf('<');
        if (generic >= 0) {
            erased = erased.substring(0, generic);
        }
        int lastDot = erased.lastIndexOf('.');
        String simple = lastDot >= 0 ? erased.substring(lastDot + 1) : erased;
        return "String".equals(simple);
    }

    private static boolean isEnumConstantCandidate(String eventStr) {
        if (eventStr == null) {
            return false;
        }
        if (eventStr.startsWith("<SYMBOLIC: ")) {
            return false;
        }
        return !eventStr.contains(" ") && !eventStr.contains("(") && !eventStr.contains(")")
                && !eventStr.contains("?");
    }

    private static boolean isWildcardVariable(String eventStr) {
        return isDynamicTriggerExpression(eventStr);
    }

    /**
     * AST-derived trigger events that must never receive package-canonical enum rewriting
     * ({@code getType()}, {@code valueOf(...)}, ternary dispatchers, parameter names, etc.).
     */
    public static boolean isDynamicTriggerExpression(String eventStr) {
        return DynamicTriggerExpressions.isDynamic(eventStr);
    }

    /**
     * Reverts mistaken package-prefixing on dynamic {@code Type.valueOf(...)} trigger expressions.
     */
    public static String denormalizeCorruptedDynamicEvent(String value) {
        if (value == null || !value.contains(".valueOf(")) {
            return value;
        }
        return value.replaceAll("(?:[a-z][a-z0-9_]*\\.)+([A-Z][A-Za-z0-9_]*)\\.valueOf\\(", "$1.valueOf(");
    }

    public enum TriggerEventKind {
        CANONICAL_ENUM,
        DYNAMIC_EXPRESSION,
        SYMBOLIC,
        LIFECYCLE,
        OTHER
    }

    public static TriggerEventKind classifyTriggerEvent(String event) {
        if (event == null || event.isBlank()) {
            return TriggerEventKind.OTHER;
        }
        if (event.startsWith("<SYMBOLIC: ")) {
            return TriggerEventKind.SYMBOLIC;
        }
        if (click.kamil.springstatemachineexporter.analysis.model.LifecycleTriggerMarkers.isLifecycle(event)) {
            return TriggerEventKind.LIFECYCLE;
        }
        if (isDynamicTriggerExpression(event)) {
            return TriggerEventKind.DYNAMIC_EXPRESSION;
        }
        if (event.contains(".")) {
            String constant = constantName(event);
            if (constant.matches("[A-Z_][A-Z0-9_]*")) {
                return TriggerEventKind.CANONICAL_ENUM;
            }
        }
        return TriggerEventKind.OTHER;
    }

    public static boolean hasCorruptedPackageDynamicEvent(String event) {
        if (event == null || !event.contains(".valueOf(")) {
            return false;
        }
        String denormalized = denormalizeCorruptedDynamicEvent(event);
        return !event.equals(denormalized);
    }

    private static List<State> canonicalizeStates(List<State> states, String stateTypeFqn, CodebaseContext context) {
        if (states == null) {
            return null;
        }
        LinkedHashMap<String, State> byFullIdentifier = new LinkedHashMap<>();
        for (State state : states) {
            State canonical = canonicalizeState(state, stateTypeFqn, context);
            byFullIdentifier.putIfAbsent(canonical.fullIdentifier(), canonical);
        }
        return new ArrayList<>(byFullIdentifier.values());
    }

    static Event canonicalizeEvent(Event event, String enumTypeFqn) {
        return canonicalizeEvent(event, enumTypeFqn, null);
    }

    static Event canonicalizeEvent(Event event, String enumTypeFqn, CodebaseContext context) {
        if (event == null || enumTypeFqn == null || enumTypeFqn.isBlank()) {
            return event;
        }
        String canonical = canonicalizeLabel(
                event.fullIdentifier() != null ? event.fullIdentifier() : event.rawName(),
                enumTypeFqn,
                context);
        String fnRaw = toFnForm(canonical, enumTypeFqn);
        if (fnRaw == null) {
            fnRaw = event.rawName();
        }
        if (canonical.equals(event.fullIdentifier()) && fnRaw.equals(event.rawName())) {
            return event;
        }
        return Event.of(fnRaw, canonical);
    }

    static State canonicalizeState(State state, String enumTypeFqn) {
        return canonicalizeState(state, enumTypeFqn, null);
    }

    static State canonicalizeState(State state, String enumTypeFqn, CodebaseContext context) {
        if (state == null || enumTypeFqn == null || enumTypeFqn.isBlank()) {
            return state;
        }
        String full = resolvePlaceholder(
                state.fullIdentifier() != null ? state.fullIdentifier() : state.rawName());
        String canonical = canonicalizeLabel(full, enumTypeFqn, context);

        String raw;
        if (isStringOrPrimitiveType(enumTypeFqn) && canonical.startsWith(enumTypeFqn + ".")) {
            int constantStart = enumTypeFqn.length() + 1;
            if (constantStart >= canonical.length()) {
                String fnRaw = toFnForm(canonical, enumTypeFqn);
                raw = fnRaw != null ? fnRaw : resolvePlaceholder(state.rawName());
            } else {
                String constant = canonical.substring(constantStart);
                raw = "\"" + constant + "\"";
            }
        } else {
            String fnRaw = toFnForm(canonical, enumTypeFqn);
            raw = fnRaw != null ? fnRaw : resolvePlaceholder(state.rawName());
        }

        if (canonical.equals(state.fullIdentifier()) && raw.equals(state.rawName())) {
            return state;
        }
        return State.of(raw, canonical);
    }

    private static String resolvePlaceholder(String value) {
        if (value == null || !value.contains("${")) {
            return value;
        }
        return click.kamil.springstatemachineexporter.analysis.util.PlaceholderResolver.resolve(
                value, java.util.Collections.emptyMap());
    }

    public static boolean isMachineEnumReference(String value, String enumTypeFqn) {
        return isMachineEnumReference(value, enumTypeFqn, null);
    }

    public static boolean isMachineEnumReference(String value, String enumTypeFqn, CodebaseContext context) {
        if (value == null || value.isBlank() || enumTypeFqn == null || enumTypeFqn.isBlank()) {
            return false;
        }
        if (isStringOrPrimitiveType(enumTypeFqn)) {
            return false;
        }
        if (value.startsWith("<SYMBOLIC: ")) {
            return false;
        }
        if (value.length() >= 2 && value.startsWith("\"")) {
            return false;
        }
        if (!isEnumConstantCandidate(value)) {
            return false;
        }
        if (Character.isLowerCase(value.charAt(0))) {
            return false;
        }

        String typePart = enumTypeFromRef(value);
        if (typePart == null) {
            return true;
        }
        if (context != null && context.isAmbiguousSimpleName(typePart)) {
            return false;
        }
        return enumTypesMatch(enumTypeFqn, typePart, context);
    }

    public static String canonicalizeLabel(String value, String enumTypeFqn) {
        return canonicalizeLabel(value, enumTypeFqn, null);
    }

    public static String canonicalizeLabel(String value, String enumTypeFqn, CodebaseContext context) {
        if (value == null || value.isBlank() || enumTypeFqn == null || enumTypeFqn.isBlank()) {
            return value;
        }
        if (isDynamicTriggerExpression(value)) {
            return denormalizeCorruptedDynamicEvent(value);
        }
        value = resolvePlaceholder(value);
        if (value == null || value.isBlank()) {
            return value;
        }
        if (value.length() >= 2 && value.startsWith("\"") && value.endsWith("\"")) {
            return stripQuotes(value);
        }

        String stripped = value;
        if (stripped == null || stripped.isBlank()) {
            return value;
        }

        if (context != null) {
            String reconstructed = TruncatedEnumRefReconstructor.reconstruct(stripped, enumTypeFqn, context);
            if (reconstructed != null && !reconstructed.equals(stripped)) {
                stripped = reconstructed;
                value = reconstructed;
            }
        }

        if (stripped.startsWith(".") || stripped.endsWith(".") || stripped.contains("..")) {
            // Truncated/malformed refs (e.g. com.example.DomainCommand.) are not canonical.
            return value;
        }

        if (stripped.startsWith(enumTypeFqn + ".")) {
            String suffix = stripped.substring(enumTypeFqn.length() + 1);
            if (!suffix.isEmpty()) {
                return stripped;
            }
            return value;
        }

        String constant = constantName(stripped);
        String typePart = enumTypeFromRef(stripped);

        if (typePart != null && !typePart.contains(".") && importStyleEnumTypeMatches(typePart, enumTypeFqn)
                && constant.matches("[A-Z_][A-Z0-9_]*")) {
            return enumTypeFqn + "." + constant;
        }

        if (typePart != null && !constant.isEmpty() && enumTypesMatch(enumTypeFqn, typePart, context)) {
            return enumTypeFqn + "." + constant;
        }

        if (typePart != null && context != null && context.isAmbiguousSimpleName(typePart)) {
            // Prefer machine-scoped qualification when preferred enum type is known.
            if (constant.matches("[A-Z_][A-Z0-9_]*")
                    && (importStyleEnumTypeMatches(typePart, enumTypeFqn)
                            || enumTypesMatch(enumTypeFqn, typePart, context)
                            || simpleName(enumTypeFqn).equals(typePart)
                            || simpleName(enumTypeFqn).equals(simpleName(typePart)))) {
                return enumTypeFqn + "." + constant;
            }
            return stripped;
        }

        if (typePart == null && Character.isUpperCase(stripped.charAt(0)) && !stripped.contains(".")) {
            if (isStringOrPrimitiveType(enumTypeFqn)) {
                return enumTypeFqn + "." + stripped;
            }
            // Only invent enumType.CONST when CONST is a real member — otherwise JMS payload
            // labels like "PAY" become OrderState.PAY and break transition linking.
            if (context != null && !enumHasConstant(enumTypeFqn, stripped, context)) {
                return stripped;
            }
            return enumTypeFqn + "." + stripped;
        }

        return stripped;
    }

    private static boolean enumHasConstant(String enumTypeFqn, String constant, CodebaseContext context) {
        if (enumTypeFqn == null || constant == null || context == null) {
            return false;
        }
        List<String> values = context.getEnumValues(stripGenerics(enumTypeFqn));
        if (values == null || values.isEmpty()) {
            return false;
        }
        for (String value : values) {
            if (value == null) {
                continue;
            }
            if (value.equals(constant) || value.endsWith("." + constant)) {
                return true;
            }
        }
        return false;
    }

    private static String stripQuotes(String value) {
        if (value.length() >= 2 && value.startsWith("\"") && value.endsWith("\"")) {
            return value.substring(1, value.length() - 1);
        }
        return value;
    }

    private static String constantName(String ref) {
        int dot = ref.lastIndexOf('.');
        return dot >= 0 ? ref.substring(dot + 1) : ref;
    }

    private static String enumTypeFromRef(String ref) {
        int dot = ref.lastIndexOf('.');
        if (dot <= 0) {
            return null;
        }
        return ref.substring(0, dot);
    }

    private static boolean importStyleEnumTypeMatches(String typePart, String enumTypeFqn) {
        String simple = simpleName(enumTypeFqn);
        return simple.equals(typePart) || (simple + "s").equals(typePart);
    }

    public static boolean enumTypesMatch(String type1, String type2) {
        if (type1 == null || type2 == null) {
            return false;
        }
        if (type1.equals(type2)) {
            return true;
        }
        if (type1.endsWith("." + type2) || type2.endsWith("." + type1)) {
            return true;
        }
        String simple1 = simpleName(type1);
        String simple2 = simpleName(type2);
        if (!simple1.equals(simple2)) {
            return false;
        }
        return !type1.contains(".") || !type2.contains(".");
    }

    public static boolean enumTypesMatch(String type1, String type2, CodebaseContext context) {
        if (context != null && type1 != null && type2 != null) {
            String simple1 = simpleName(type1);
            String simple2 = simpleName(type2);
            if (simple1.equals(simple2) && context.isAmbiguousSimpleName(simple1)) {
                return type1.equals(type2);
            }
        }
        return enumTypesMatch(type1, type2);
    }

    /**
     * Whether a concrete polymorphic enum constant belongs to the configured machine event type.
     * Uses machine context to accept import-style labels even when the enum simple name is
     * ambiguous across the scanned codebase.
     */
    public static boolean polymorphicEventMatchesMachineEventType(
            String polymorphicEvent,
            String machineEventTypeFqn,
            CodebaseContext context) {
        if (polymorphicEvent == null || machineEventTypeFqn == null || !polymorphicEvent.contains(".")) {
            return false;
        }
        if (polymorphicEvent.startsWith("<SYMBOLIC:") || polymorphicEvent.startsWith("ENUM_SET:")) {
            return false;
        }
        String enumType = polymorphicEvent.substring(0, polymorphicEvent.lastIndexOf('.'));
        if (machineEventTypeFqn.equals(enumType)) {
            return true;
        }
        if (importStyleEnumTypeMatches(enumType, machineEventTypeFqn)) {
            return true;
        }
        return enumType.contains(".") && enumTypesMatch(machineEventTypeFqn, enumType, context);
    }

    public static boolean enumTypesMatchForMachine(
            String machineEnumFqn,
            String candidateType,
            CodebaseContext context) {
        if (machineEnumFqn == null || candidateType == null) {
            return false;
        }
        if (machineEnumFqn.equals(candidateType)) {
            return true;
        }
        if (importStyleEnumTypeMatches(candidateType, machineEnumFqn)) {
            return true;
        }
        return candidateType.contains(".") && enumTypesMatch(machineEnumFqn, candidateType, context);
    }

    private static String simpleName(String fqn) {
        return fqn.contains(".") ? fqn.substring(fqn.lastIndexOf('.') + 1) : fqn;
    }

    /**
     * Derives {@code EnumFormat.fn} display form ({@code OrderEvent.PAY}) from a package-canonical identifier.
     */
    public static String expectedFnRawName(String canonicalFqn, String enumTypeFqn) {
        return toFnForm(canonicalFqn, enumTypeFqn);
    }

    /**
     * Returns whether {@code rawName} is the fn-form counterpart of a package-canonical {@code fullIdentifier}.
     */
    public static boolean isRawNameConsistentWithFullIdentifier(
            String rawName,
            String fullIdentifier,
            String enumTypeFqn) {
        if (fullIdentifier == null || fullIdentifier.isBlank() || isStringOrPrimitiveType(enumTypeFqn)) {
            return true;
        }
        if (fullIdentifier.startsWith("<") || fullIdentifier.startsWith("\"")) {
            return true;
        }
        if (isDynamicTriggerExpression(fullIdentifier)) {
            return true;
        }
        String canonical = canonicalizeLabel(fullIdentifier, enumTypeFqn);
        if (!canonical.equals(fullIdentifier)) {
            return true;
        }
        if (!constantName(fullIdentifier).matches("[A-Z_][A-Z0-9_]*")) {
            return true;
        }
        String expectedRaw = toFnForm(fullIdentifier, enumTypeFqn);
        if (expectedRaw == null) {
            return true;
        }
        return expectedRaw.equals(rawName);
    }

    /**
     * Polymorphic event lists must contain only package-canonical enum constants or symbolic wildcards.
     */
    public static boolean isValidPolymorphicEventForm(String event) {
        if (event == null || event.isBlank()) {
            return false;
        }
        TriggerEventKind kind = classifyTriggerEvent(event);
        if (kind != TriggerEventKind.CANONICAL_ENUM && kind != TriggerEventKind.SYMBOLIC) {
            return false;
        }
        return !hasCorruptedPackageDynamicEvent(event);
    }

    /**
     * True when every entry is a concrete package-canonical enum constant (not symbolic/dynamic).
     */
    public static boolean hasOnlyConcretePolymorphicEvents(List<String> polymorphicEvents) {
        if (polymorphicEvents == null || polymorphicEvents.isEmpty()) {
            return false;
        }
        return polymorphicEvents.stream()
                .allMatch(pe -> classifyTriggerEvent(pe) == TriggerEventKind.CANONICAL_ENUM);
    }

    static String toFnForm(String canonicalFqn, String enumTypeFqn) {
        if (canonicalFqn == null || canonicalFqn.isBlank() || isStringOrPrimitiveType(enumTypeFqn)) {
            return null;
        }
        if (canonicalFqn.startsWith("<") || canonicalFqn.startsWith("\"")) {
            return null;
        }
        if (!canonicalFqn.contains(".")) {
            return canonicalFqn;
        }
        int lastDot = canonicalFqn.lastIndexOf('.');
        int prevDot = canonicalFqn.lastIndexOf('.', lastDot - 1);
        if (prevDot > 0) {
            return canonicalFqn.substring(prevDot + 1);
        }
        return simpleName(enumTypeFqn) + "." + canonicalFqn.substring(lastDot + 1);
    }

    /**
     * When path bindings prove a single {@code event} literal (e.g. {@code "PAY".equalsIgnoreCase(event)})
     * and the trigger is {@code Enum.valueOf(...)}, synthesize one concrete polymorphic event for linking.
     */
    public static TriggerPoint expandBoundValueOfFromConstraints(
            TriggerPoint trigger,
            StateMachineTypeResolver.MachineTypes machineTypes,
            CodebaseContext context) {
        return expandBoundValueOfFromConstraints(trigger, machineTypes, context, null);
    }

    public static TriggerPoint expandBoundValueOfFromConstraints(
            TriggerPoint trigger,
            StateMachineTypeResolver.MachineTypes machineTypes,
            CodebaseContext context,
            Map<String, String> pathBindings) {
        if (trigger == null || trigger.getEvent() == null || machineTypes == null) {
            return trigger;
        }
        if (!isDynamicTriggerExpression(trigger.getEvent()) || !trigger.getEvent().contains(".valueOf(")) {
            return trigger;
        }
        List<String> boundEventLiterals = ConstraintClauses.boundCarrierLiterals(
                trigger.getConstraint(), pathBindings);
        // Dataflow-only: never invent event constants from REST URL path segments /
        // unbound templates — only concrete expander bindings or constraint literals.
        if (boundEventLiterals.size() != 1) {
            return trigger;
        }
        String eventTypeFqn = machineTypes.eventTypeFqn();
        if (eventTypeFqn == null || eventTypeFqn.isBlank()) {
            return trigger;
        }
        String literal = boundEventLiterals.get(0);
        // Command keys like "order.pay" are not enum constants — take the trailing segment.
        int lastDot = literal.lastIndexOf('.');
        if (lastDot >= 0 && lastDot + 1 < literal.length()) {
            literal = literal.substring(lastDot + 1);
        }
        literal = literal.toUpperCase();
        String constantFqn = eventTypeFqn + "." + literal;
        if (context != null) {
            List<String> machineEnumValues = context.getEnumValues(eventTypeFqn);
            if (machineEnumValues == null || machineEnumValues.stream().noneMatch(constantFqn::equals)) {
                return trigger;
            }
        }
        return trigger.toBuilder()
                .polymorphicEvents(List.of(constantFqn))
                .ambiguous(false)
                .external(false)
                .build();
    }

    /**
     * True when the trigger is {@code Enum.valueOf(...)} without a single bound carrier literal
     * and without concrete polymorphic events — eligible for enum-wide / transition-wide widen.
     */
    private static boolean isUnboundValueOfWidenCandidate(
            TriggerPoint trigger, Map<String, String> pathBindings) {
        if (trigger == null || trigger.getEvent() == null) {
            return false;
        }
        if (!isDynamicTriggerExpression(trigger.getEvent()) || !trigger.getEvent().contains(".valueOf(")) {
            return false;
        }
        // Pre-marked ambiguous without a binding stays fail-closed (cross-package / unresolved).
        if (trigger.isAmbiguous()) {
            return false;
        }
        if (hasConcretePolymorphicEvents(trigger.getPolymorphicEvents())) {
            return false;
        }
        List<String> bound = ConstraintClauses.boundCarrierLiterals(trigger.getConstraint(), pathBindings);
        return bound.size() != 1;
    }

    private static List<String> widenUnboundValueOfEvents(
            String eventTypeFqn,
            List<Transition> machineTransitions,
            CodebaseContext context) {
        if (eventTypeFqn == null || eventTypeFqn.isBlank()) {
            return List.of();
        }
        List<String> transitionEvents =
                polymorphicEventsFromTransitions(machineTransitions, eventTypeFqn, context);
        if (!transitionEvents.isEmpty()) {
            return transitionEvents;
        }
        if (context == null) {
            return List.of();
        }
        List<String> enumValues = context.getEnumValues(eventTypeFqn);
        if (enumValues == null || enumValues.isEmpty()) {
            return List.of();
        }
        return List.copyOf(enumValues);
    }
}
