package click.kamil.springstatemachineexporter.analysis.validation;

import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.LifecycleTriggerMarkers;
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.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.List;
import java.util.Set;

/**
 * Validates that enum-backed identifiers across the analysis model share one canonical shape:
 * {@code {package.EnumType.CONSTANT}} derived from the machine config's {@code <State, Event>} types.
 *
 * <p>String/primitive machines ({@code StateMachineConfigurerAdapter<String, String>}) are skipped.
 */
public final class AnalysisCanonicalFormValidator {

    public record Violation(String path, String actual, String expected) {
    }

    private AnalysisCanonicalFormValidator() {
    }

    public static List<Violation> validate(AnalysisResult result, CodebaseContext context) {
        List<Violation> violations = new ArrayList<>();
        if (result == null || context == null) {
            return violations;
        }

        StateMachineTypeResolver.MachineTypes machineTypes =
                StateMachineTypeResolver.resolveTypes(result.getName(), context);
        if (!hasPackageQualifiedEnumType(machineTypes.stateTypeFqn())
                && !hasPackageQualifiedEnumType(machineTypes.eventTypeFqn())) {
            if (!StateMachineTypeResolver.extendsEnumStateMachineConfigurer(result.getName(), context)) {
                return violations;
            }
            violations.add(new Violation(
                    "machineTypes",
                    describeTypes(machineTypes),
                    "package-qualified state/event enum types"));
            return violations;
        }

        validateFields(result, machineTypes, violations, context);
        return violations;
    }

    public static List<Violation> validateWithMachineTypes(
            AnalysisResult result,
            StateMachineTypeResolver.MachineTypes machineTypes) {
        List<Violation> violations = new ArrayList<>();
        if (result == null || machineTypes == null) {
            return violations;
        }
        if (!hasPackageQualifiedEnumType(machineTypes.stateTypeFqn())
                && !hasPackageQualifiedEnumType(machineTypes.eventTypeFqn())) {
            return violations;
        }

        validateFields(result, machineTypes, violations, null);
        return violations;
    }

    public static void enforce(AnalysisResult result, CodebaseContext context) {
        List<Violation> violations = validate(result, context);
        if (!violations.isEmpty()) {
            throw new AnalysisCanonicalFormException(violations);
        }
    }

    public static void enforceWithMachineTypes(
            AnalysisResult result,
            StateMachineTypeResolver.MachineTypes machineTypes) {
        List<Violation> violations = validateWithMachineTypes(result, machineTypes);
        if (!violations.isEmpty()) {
            throw new AnalysisCanonicalFormException(violations);
        }
    }

    private static void validateFields(
            AnalysisResult result,
            StateMachineTypeResolver.MachineTypes machineTypes,
            List<Violation> violations,
            CodebaseContext context) {
        validateTransitions(result.getTransitions(), machineTypes, violations, context);
        validateStateCollection(result.getStates(), machineTypes.stateTypeFqn(), "states", violations, context);
        validateStateRawNameSync(result.getStates(), machineTypes.stateTypeFqn(), "states", violations);
        validateStateLabels(result.getStartStates(), machineTypes.stateTypeFqn(), "startStates", violations, context);
        validateStateLabels(result.getEndStates(), machineTypes.stateTypeFqn(), "endStates", violations, context);

        if (result.getMetadata() != null) {
            validateTriggers(result.getMetadata().getTriggers(), machineTypes, violations, context);
            validateCallChains(
                    result.getMetadata().getCallChains(), result.getTransitions(), machineTypes, violations, context);
        }
    }

    private static String describeTypes(StateMachineTypeResolver.MachineTypes machineTypes) {
        return "stateTypeFqn=" + machineTypes.stateTypeFqn() + ", eventTypeFqn=" + machineTypes.eventTypeFqn();
    }

    private static boolean hasPackageQualifiedEnumType(String typeFqn) {
        return typeFqn != null && typeFqn.contains(".") && !MachineEnumCanonicalizer.isStringOrPrimitiveType(typeFqn);
    }

    private static void validateTransitions(
            List<Transition> transitions,
            StateMachineTypeResolver.MachineTypes machineTypes,
            List<Violation> violations,
            CodebaseContext context) {
        if (transitions == null) {
            return;
        }
        for (int i = 0; i < transitions.size(); i++) {
            Transition transition = transitions.get(i);
            String prefix = "transitions[" + i + "]";
            if (transition.getEvent() != null) {
                requireCanonical(
                        prefix + ".event.fullIdentifier",
                        transition.getEvent().fullIdentifier(),
                        machineTypes.eventTypeFqn(),
                        violations,
                        context);
                requireRawNameSync(
                        prefix + ".event.rawName",
                        transition.getEvent().rawName(),
                        transition.getEvent().fullIdentifier(),
                        machineTypes.eventTypeFqn(),
                        violations);
            }
            validateStateListWithRawSync(
                    transition.getSourceStates(), machineTypes.stateTypeFqn(), prefix + ".sourceStates", violations, context);
            validateStateListWithRawSync(
                    transition.getTargetStates(), machineTypes.stateTypeFqn(), prefix + ".targetStates", violations, context);
        }
    }

    private static void validateStateListWithRawSync(
            List<State> states,
            String stateTypeFqn,
            String pathPrefix,
            List<Violation> violations,
            CodebaseContext context) {
        if (states == null) {
            return;
        }
        for (int i = 0; i < states.size(); i++) {
            State state = states.get(i);
            requireCanonical(pathPrefix + "[" + i + "].fullIdentifier", state.fullIdentifier(), stateTypeFqn, violations, context);
            requireRawNameSync(
                    pathPrefix + "[" + i + "].rawName",
                    state.rawName(),
                    state.fullIdentifier(),
                    stateTypeFqn,
                    violations);
        }
    }
    private static void validateStateRawNameSync(
            Set<State> states,
            String stateTypeFqn,
            String pathPrefix,
            List<Violation> violations) {
        if (states == null) {
            return;
        }
        int i = 0;
        for (State state : states) {
            requireRawNameSync(
                    pathPrefix + "[" + i++ + "].rawName",
                    state.rawName(),
                    state.fullIdentifier(),
                    stateTypeFqn,
                    violations);
        }
    }

    private static void validateStateCollection(
            Set<State> states,
            String stateTypeFqn,
            String pathPrefix,
            List<Violation> violations,
            CodebaseContext context) {
        if (states == null) {
            return;
        }
        int i = 0;
        for (State state : states) {
            requireCanonical(pathPrefix + "[" + i++ + "].fullIdentifier", state.fullIdentifier(), stateTypeFqn, violations, context);
        }
    }

    private static void validateStateLabels(
            Set<String> labels,
            String stateTypeFqn,
            String pathPrefix,
            List<Violation> violations,
            CodebaseContext context) {
        if (labels == null) {
            return;
        }
        int i = 0;
        for (String label : labels) {
            requireCanonical(pathPrefix + "[" + i++ + "]", label, stateTypeFqn, violations, context);
        }
    }

    private static void validateTriggers(
            List<TriggerPoint> triggers,
            StateMachineTypeResolver.MachineTypes machineTypes,
            List<Violation> violations,
            CodebaseContext context) {
        if (triggers == null) {
            return;
        }
        for (int i = 0; i < triggers.size(); i++) {
            TriggerPoint trigger = triggers.get(i);
            String prefix = "metadata.triggers[" + i + "]";
            String eventTypeFqn = preferTypeFqn(trigger.getEventTypeFqn(), machineTypes.eventTypeFqn());
            String stateTypeFqn = preferTypeFqn(trigger.getStateTypeFqn(), machineTypes.stateTypeFqn());

            requireCanonical(prefix + ".event", trigger.getEvent(), eventTypeFqn, violations, context);
            validateTriggerEventForm(prefix + ".event", trigger.getEvent(), violations);
            requireCanonical(prefix + ".sourceState", trigger.getSourceState(), stateTypeFqn, violations, context);
            validateDynamicIdentifierForm(prefix + ".sourceState", trigger.getSourceState(), violations);

            if (trigger.getPolymorphicEvents() != null) {
                for (int j = 0; j < trigger.getPolymorphicEvents().size(); j++) {
                    String polyPath = prefix + ".polymorphicEvents[" + j + "]";
                    String polyEvent = trigger.getPolymorphicEvents().get(j);
                    requireCanonical(polyPath, polyEvent, eventTypeFqn, violations, context);
                    validatePolymorphicEventForm(polyPath, polyEvent, violations);
                }
            }
        }
    }

    private static void validateCallChains(
            List<CallChain> callChains,
            List<Transition> transitions,
            StateMachineTypeResolver.MachineTypes machineTypes,
            List<Violation> violations,
            CodebaseContext context) {
        if (callChains == null) {
            return;
        }
        for (int i = 0; i < callChains.size(); i++) {
            CallChain chain = callChains.get(i);
            String prefix = "metadata.callChains[" + i + "]";
            TriggerPoint trigger = chain.getTriggerPoint();
            if (trigger != null) {
                String triggerPrefix = prefix + ".triggerPoint";
                String eventTypeFqn = preferTypeFqn(trigger.getEventTypeFqn(), machineTypes.eventTypeFqn());
                String stateTypeFqn = preferTypeFqn(trigger.getStateTypeFqn(), machineTypes.stateTypeFqn());
                requireCanonical(triggerPrefix + ".event", trigger.getEvent(), eventTypeFqn, violations, context);
                validateTriggerEventForm(triggerPrefix + ".event", trigger.getEvent(), violations);
                requireCanonical(triggerPrefix + ".sourceState", trigger.getSourceState(), stateTypeFqn, violations, context);
                validateDynamicIdentifierForm(triggerPrefix + ".sourceState", trigger.getSourceState(), violations);
                if (trigger.getPolymorphicEvents() != null) {
                    for (int j = 0; j < trigger.getPolymorphicEvents().size(); j++) {
                        String polyPath = triggerPrefix + ".polymorphicEvents[" + j + "]";
                        String polyEvent = trigger.getPolymorphicEvents().get(j);
                        requireCanonical(polyPath, polyEvent, eventTypeFqn, violations, context);
                        validatePolymorphicEventForm(polyPath, polyEvent, violations);
                    }
                }
                validateMatchedTransitionsWhenResolvable(
                        prefix, chain, trigger, transitions, machineTypes.eventTypeFqn(), violations, context);
                validateOverLinkedPolymorphicEvents(
                        prefix, chain, trigger, transitions, machineTypes.eventTypeFqn(), violations, context);
            }
            if (chain.getMatchedTransitions() != null) {
                for (int j = 0; j < chain.getMatchedTransitions().size(); j++) {
                    MatchedTransition matched = chain.getMatchedTransitions().get(j);
                    String matchedPrefix = prefix + ".matchedTransitions[" + j + "]";
                    requireCanonical(matchedPrefix + ".event", matched.getEvent(), machineTypes.eventTypeFqn(), violations, context);
                    requireCanonical(matchedPrefix + ".sourceState", matched.getSourceState(), machineTypes.stateTypeFqn(), violations, context);
                    requireCanonical(matchedPrefix + ".targetState", matched.getTargetState(), machineTypes.stateTypeFqn(), violations, context);
                }
            }
        }
    }

    private static String preferTypeFqn(String primary, String fallback) {
        if (hasPackageQualifiedEnumType(primary)) {
            return primary;
        }
        return fallback;
    }

    private static void requireCanonical(
            String path,
            String value,
            String enumTypeFqn,
            List<Violation> violations,
            CodebaseContext context) {
        if (!MachineEnumCanonicalizer.isMachineEnumReference(value, enumTypeFqn, context)) {
            return;
        }
        String expected = MachineEnumCanonicalizer.canonicalizeLabel(value, enumTypeFqn, context);
        if (!expected.equals(value)) {
            violations.add(new Violation(path, value, expected));
        }
    }

    private static void requireRawNameSync(
            String path,
            String rawName,
            String fullIdentifier,
            String enumTypeFqn,
            List<Violation> violations) {
        if (MachineEnumCanonicalizer.isRawNameConsistentWithFullIdentifier(rawName, fullIdentifier, enumTypeFqn)) {
            return;
        }
        String expected = MachineEnumCanonicalizer.expectedFnRawName(fullIdentifier, enumTypeFqn);
        violations.add(new Violation(path, rawName, expected));
    }

    private static void validatePolymorphicEventForm(String path, String event, List<Violation> violations) {
        if (event == null || event.isBlank()) {
            return;
        }
        if (!MachineEnumCanonicalizer.isValidPolymorphicEventForm(event)) {
            MachineEnumCanonicalizer.TriggerEventKind kind = MachineEnumCanonicalizer.classifyTriggerEvent(event);
            String expected = kind == MachineEnumCanonicalizer.TriggerEventKind.DYNAMIC_EXPRESSION
                    && MachineEnumCanonicalizer.hasCorruptedPackageDynamicEvent(event)
                    ? MachineEnumCanonicalizer.denormalizeCorruptedDynamicEvent(event)
                    : "CANONICAL_ENUM or SYMBOLIC";
            violations.add(new Violation(path, event, expected));
        }
    }

    private static void validateDynamicIdentifierForm(String path, String value, List<Violation> violations) {
        if (value == null || value.isBlank()) {
            return;
        }
        if (MachineEnumCanonicalizer.hasCorruptedPackageDynamicEvent(value)) {
            violations.add(new Violation(
                    path,
                    value,
                    MachineEnumCanonicalizer.denormalizeCorruptedDynamicEvent(value)));
        }
    }

    private static void validateOverLinkedPolymorphicEvents(
            String chainPrefix,
            CallChain chain,
            TriggerPoint trigger,
            List<Transition> transitions,
            String eventTypeFqn,
            List<Violation> violations,
            CodebaseContext context) {
        if (trigger.getPolymorphicEvents() == null || trigger.getPolymorphicEvents().isEmpty()) {
            return;
        }
        if (!MachineEnumCanonicalizer.hasOnlyConcretePolymorphicEvents(trigger.getPolymorphicEvents())) {
            return;
        }
        List<String> transitionEvents =
                MachineEnumCanonicalizer.polymorphicEventsFromTransitions(transitions, eventTypeFqn, context);
        if (transitionEvents.isEmpty()) {
            return;
        }
        List<String> extras = new ArrayList<>();
        for (String polyEvent : trigger.getPolymorphicEvents()) {
            if (!transitionEvents.contains(polyEvent)) {
                extras.add(polyEvent);
            }
        }
        if (!extras.isEmpty()) {
            violations.add(new Violation(
                    chainPrefix + ".triggerPoint.polymorphicEvents",
                    String.valueOf(trigger.getPolymorphicEvents().size()),
                    "subset of configured transition events; unexpected: " + extras));
            return;
        }
        int matchingTransitionCount = countMatchingConfiguredTransitions(
                transitions, trigger.getPolymorphicEvents(), eventTypeFqn, context);
        if (matchingTransitionCount > 0
                && chain.getMatchedTransitions() != null
                && chain.getMatchedTransitions().size() > matchingTransitionCount) {
            violations.add(new Violation(
                    chainPrefix + ".matchedTransitions",
                    String.valueOf(chain.getMatchedTransitions().size()),
                    "at most " + matchingTransitionCount + " for configured transitions"));
        }
    }

    private static int countMatchingConfiguredTransitions(
            List<Transition> transitions,
            List<String> polymorphicEvents,
            String eventTypeFqn,
            CodebaseContext context) {
        if (transitions == null || transitions.isEmpty() || polymorphicEvents == null) {
            return 0;
        }
        int count = 0;
        for (Transition transition : transitions) {
            if (transition.getEvent() == null) {
                continue;
            }
            String smEvent = transition.getEvent().fullIdentifier() != null
                    ? transition.getEvent().fullIdentifier()
                    : transition.getEvent().rawName();
            for (String polyEvent : polymorphicEvents) {
                if (eventsMatch(polyEvent, smEvent, eventTypeFqn, context)) {
                    count++;
                    break;
                }
            }
        }
        return count;
    }

    private static void validateMatchedTransitionsWhenResolvable(
            String chainPrefix,
            CallChain chain,
            TriggerPoint trigger,
            List<Transition> transitions,
            String eventTypeFqn,
            List<Violation> violations,
            CodebaseContext context) {
        if (LifecycleTriggerMarkers.isLifecycle(trigger.getEvent())) {
            return;
        }
        if (isIntentionalAmbiguousFailClosed(trigger)) {
            return;
        }
        if (chain.getMatchedTransitions() != null && !chain.getMatchedTransitions().isEmpty()) {
            return;
        }
        if (chain.getLinkResolution() == click.kamil.springstatemachineexporter.analysis.model.LinkResolution.NO_MATCH
                || chain.getLinkResolution()
                        == click.kamil.springstatemachineexporter.analysis.model.LinkResolution.UNRESOLVED_EXTERNAL) {
            return;
        }

        MachineEnumCanonicalizer.TriggerEventKind eventKind =
                MachineEnumCanonicalizer.classifyTriggerEvent(trigger.getEvent());
        if (eventKind == MachineEnumCanonicalizer.TriggerEventKind.CANONICAL_ENUM) {
            if (eventMatchesAnyTransition(trigger.getEvent(), transitions, eventTypeFqn, context)) {
                violations.add(new Violation(
                        chainPrefix + ".matchedTransitions",
                        "empty",
                        "non-empty when trigger has concrete machine enum event matching transitions"));
            }
            return;
        }

        if (!MachineEnumCanonicalizer.hasOnlyConcretePolymorphicEvents(trigger.getPolymorphicEvents())) {
            return;
        }
        if (eventKind != MachineEnumCanonicalizer.TriggerEventKind.DYNAMIC_EXPRESSION) {
            return;
        }
        if (!polymorphicEventsMatchAnyTransition(trigger.getPolymorphicEvents(), transitions, eventTypeFqn, context)) {
            return;
        }
        violations.add(new Violation(
                chainPrefix + ".matchedTransitions",
                "empty",
                "non-empty when dynamic trigger has concrete polymorphicEvents matching transitions"));
    }

    private static boolean isIntentionalAmbiguousFailClosed(TriggerPoint trigger) {
        if (trigger == null) {
            return false;
        }
        if (trigger.isAmbiguous() && !trigger.isExternal()) {
            return true;
        }
        String event = trigger.getEvent();
        if (event != null && event.startsWith("ENUM_SET:")) {
            return true;
        }
        if (trigger.isExternal()
                && trigger.isAmbiguous()
                && DynamicTriggerExpressions.isDynamic(event)
                && (trigger.getPolymorphicEvents() == null || trigger.getPolymorphicEvents().isEmpty())) {
            return true;
        }
        return false;
    }

    private static boolean eventMatchesAnyTransition(
            String triggerEvent,
            List<Transition> transitions,
            String eventTypeFqn,
            CodebaseContext context) {
        if (transitions == null || transitions.isEmpty() || triggerEvent == null) {
            return false;
        }
        for (Transition transition : transitions) {
            if (transition.getEvent() == null) {
                continue;
            }
            String smEvent = transition.getEvent().fullIdentifier() != null
                    ? transition.getEvent().fullIdentifier()
                    : transition.getEvent().rawName();
            if (eventsMatch(triggerEvent, smEvent, eventTypeFqn, context)) {
                return true;
            }
        }
        return false;
    }

    private static boolean polymorphicEventsMatchAnyTransition(
            List<String> polymorphicEvents,
            List<Transition> transitions,
            String eventTypeFqn,
            CodebaseContext context) {
        if (transitions == null || transitions.isEmpty()) {
            return false;
        }
        for (String polyEvent : polymorphicEvents) {
            for (Transition transition : transitions) {
                if (transition.getEvent() == null) {
                    continue;
                }
                String smEvent = transition.getEvent().fullIdentifier() != null
                        ? transition.getEvent().fullIdentifier()
                        : transition.getEvent().rawName();
                if (eventsMatch(polyEvent, smEvent, eventTypeFqn, context)) {
                    return true;
                }
            }
        }
        return false;
    }

    private static boolean eventsMatch(
            String triggerEvent,
            String smEvent,
            String eventTypeFqn,
            CodebaseContext context) {
        if (triggerEvent == null || smEvent == null) {
            return false;
        }
        if (triggerEvent.equals(smEvent)) {
            return true;
        }
        String triggerConst = constantName(triggerEvent);
        String smConst = constantName(smEvent);
        if (!triggerConst.equals(smConst)) {
            return false;
        }
        if (MachineEnumCanonicalizer.isMachineEnumReference(triggerEvent, eventTypeFqn, context)
                && MachineEnumCanonicalizer.isMachineEnumReference(smEvent, eventTypeFqn, context)) {
            return true;
        }
        return false;
    }

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

    private static void validateTriggerEventForm(String path, String event, List<Violation> violations) {
        if (event == null || event.isBlank()) {
            return;
        }
        if (MachineEnumCanonicalizer.hasCorruptedPackageDynamicEvent(event)) {
            violations.add(new Violation(
                    path,
                    event,
                    MachineEnumCanonicalizer.denormalizeCorruptedDynamicEvent(event)));
        }
    }
}
