package click.kamil.springstatemachineexporter.analysis.enricher;

import click.kamil.springstatemachineexporter.analysis.enricher.path.SymbolicPathValueEstimator;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
import click.kamil.springstatemachineexporter.analysis.resolver.ConstraintClauses;
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.EventCarrierPolicy;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import click.kamil.springstatemachineexporter.model.Transition;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.TypeDeclaration;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Narrows wide call-graph polymorphic widens using dataflow evidence only.
 *
 * <p>Evidence sources: static trigger events, path-binding map values, branch constraints,
 * sendEvent enum literals along the method chain, and AST path-value estimation.
 * REST URL path segments and method-name tokens are intentionally not used — those are not
 * dataflow and falsely link endpoints to transitions.
 */
public final class CallChainPolyNarrower {

    private static final int STRONG = 3;
    private static final int MEDIUM = 2;
    private static final int WEAK = 1;

    private static final Pattern ENUM_EQ = Pattern.compile(
            "([A-Za-z_][\\w.]*)\\s*==\\s*([A-Za-z_][\\w.]*)");
    private static final Pattern ROUTE_LITERAL = Pattern.compile(
            "route\\(\\s*\"([^\"]+)\"\\s*,\\s*\"([^\"]+)\"\\s*\\)");

    private static final Set<String> FIRE_METHOD_NAMES = Set.of(
            "sendEvent", "fire", "fireEvent", "dispatchEvent", "send", "trigger");

    private CallChainPolyNarrower() {
    }

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

    public static TriggerPoint narrowForCallChain(
            TriggerPoint trigger,
            CallChain chain,
            StateMachineTypeResolver.MachineTypes machineTypes,
            String embeddedEventTypeFqn,
            List<Transition> machineTransitions,
            CodebaseContext context) {
        if (trigger == null) {
            return trigger;
        }
        List<String> poly = trigger.getPolymorphicEvents();
        if (poly == null || poly.size() <= 1) {
            return trigger;
        }

        String eventTypeFqn = CallChainLinkPolicy.resolveMachineEventTypeFqn(
                null,
                embeddedEventTypeFqn != null ? embeddedEventTypeFqn : trigger.getEventTypeFqn(),
                machineTypes,
                machineTransitions,
                context);
        if (eventTypeFqn == null || eventTypeFqn.isBlank()) {
            return trigger;
        }
        Set<String> configuredConstants = configuredEventConstants(machineTransitions, eventTypeFqn, context);
        if (configuredConstants.isEmpty()) {
            return trigger;
        }

        HintVotes votes = new HintVotes();
        collectHints(trigger, chain, configuredConstants, context, votes);

        String chosen = votes.choose(configuredConstants);
        if (chosen == null) {
            return trigger;
        }

        List<String> narrowed = filterPolyToConstant(poly, chosen, eventTypeFqn, context);
        if (narrowed.size() == 1) {
            return trigger.toBuilder()
                    .polymorphicEvents(narrowed)
                    .ambiguous(false)
                    .build();
        }

        return trigger.toBuilder()
                .polymorphicEvents(List.of(eventTypeFqn + "." + chosen))
                .ambiguous(false)
                .build();
    }

    private static void collectHints(
            TriggerPoint trigger,
            CallChain chain,
            Set<String> configuredConstants,
            CodebaseContext context,
            HintVotes votes) {
        collectFromStaticTriggerEvent(trigger, configuredConstants, votes);
        collectFromConstraint(trigger.getConstraint(), configuredConstants, votes);
        if (chain != null) {
            collectFromPathBindings(chain.getPathBindings(), configuredConstants, votes);
            collectFromSendEventLiterals(chain.getMethodChain(), configuredConstants, context, votes);
            collectFromPathEstimator(chain, configuredConstants, context, votes);
        }
    }

    private static void collectFromStaticTriggerEvent(
            TriggerPoint trigger,
            Set<String> configuredConstants,
            HintVotes votes) {
        String event = trigger.getEvent();
        if (event == null || event.isBlank()) {
            return;
        }
        if (DynamicTriggerExpressions.isDynamic(event)) {
            return;
        }
        addEnumReference(event, configuredConstants, votes, STRONG);
    }

    /**
     * Path bindings come from {@code PathBindingEvaluator} / call-graph dataflow, not from URL text.
     */
    private static void collectFromPathBindings(
            Map<String, String> pathBindings,
            Set<String> configuredConstants,
            HintVotes votes) {
        if (pathBindings == null || pathBindings.isEmpty()) {
            return;
        }
        for (Map.Entry<String, String> entry : pathBindings.entrySet()) {
            String key = entry.getKey();
            String value = entry.getValue();
            if (value == null || value.isBlank()) {
                continue;
            }
            boolean eventLikeKey = EventCarrierPolicy.isCarrierParamName(key);
            int weight = eventLikeKey ? STRONG : MEDIUM;
            addEnumReference(value, configuredConstants, votes, weight);
            addPathToken(value, configuredConstants, votes, weight);
        }
    }

    private static void collectFromConstraint(
            String constraint,
            Set<String> configuredConstants,
            HintVotes votes) {
        if (constraint == null || constraint.isBlank()) {
            return;
        }
        for (String literal : ConstraintClauses.carrierLiterals(constraint)) {
            addPathToken(literal, configuredConstants, votes, MEDIUM);
            if (literal.contains(".")) {
                addPathToken(literal, configuredConstants, votes, MEDIUM);
            }
        }
        Matcher route = ROUTE_LITERAL.matcher(constraint);
        while (route.find()) {
            addPathToken(route.group(2), configuredConstants, votes, MEDIUM);
        }
        Matcher enumEq = ENUM_EQ.matcher(constraint);
        while (enumEq.find()) {
            addEnumReference(enumEq.group(1), configuredConstants, votes, MEDIUM);
            addEnumReference(enumEq.group(2), configuredConstants, votes, MEDIUM);
        }
    }

    private static void collectFromSendEventLiterals(
            List<String> methodChain,
            Set<String> configuredConstants,
            CodebaseContext context,
            HintVotes votes) {
        if (context == null || methodChain == null) {
            return;
        }
        ConstantResolver resolver = new ConstantResolver();
        for (String methodFqn : methodChain) {
            if (methodFqn == null || !methodFqn.contains(".")) {
                continue;
            }
            String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
            String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
            TypeDeclaration typeDeclaration = context.getTypeDeclaration(className);
            if (typeDeclaration == null) {
                continue;
            }
            MethodDeclaration methodDeclaration = context.findMethodDeclaration(typeDeclaration, methodName, true);
            if (methodDeclaration == null) {
                continue;
            }
            for (Object parameter : methodDeclaration.parameters()) {
                if (parameter instanceof SingleVariableDeclaration variable) {
                    addUniqueIdentifierMatch(variable.getType().toString(), configuredConstants, votes, MEDIUM);
                }
            }
            if (methodDeclaration.getBody() == null) {
                continue;
            }
            methodDeclaration.getBody().accept(new ASTVisitor() {
                @Override
                public boolean visit(MethodInvocation node) {
                    boolean fireSite = FIRE_METHOD_NAMES.contains(node.getName().getIdentifier());
                    for (Object argument : node.arguments()) {
                        if (!(argument instanceof Expression expression)) {
                            continue;
                        }
                        if (expression instanceof QualifiedName qualifiedName) {
                            // Enum constant passed into the next hop (dataflow at the call site).
                            addEnumReference(
                                    qualifiedName.getFullyQualifiedName(),
                                    configuredConstants,
                                    votes,
                                    STRONG);
                        } else if (fireSite && expression instanceof SimpleName simpleName) {
                            // Only accept UPPERCASE identifiers that are configured constants —
                            // plain parameter names like "event" must not vote.
                            String identifier = simpleName.getIdentifier();
                            if (identifier != null
                                    && identifier.equals(identifier.toUpperCase(Locale.ROOT))
                                    && configuredConstants.contains(identifier)) {
                                addEnumReference(identifier, configuredConstants, votes, STRONG);
                            }
                        } else if (fireSite) {
                            String resolved = resolver.resolve(expression, context);
                            if (resolved != null) {
                                addEnumReference(resolved, configuredConstants, votes, STRONG);
                            }
                        }
                    }
                    return super.visit(node);
                }
            });
        }
    }

    private static void collectFromPathEstimator(
            CallChain chain,
            Set<String> configuredConstants,
            CodebaseContext context,
            HintVotes votes) {
        if (context == null) {
            return;
        }
        List<String> estimated = new SymbolicPathValueEstimator().estimatePossibleValues(chain, context);
        if (estimated == null || estimated.isEmpty()) {
            return;
        }
        int weight = estimated.size() == 1 ? MEDIUM : WEAK;
        for (String value : estimated) {
            addEnumReference(value, configuredConstants, votes, weight);
        }
    }

    private static void addPathToken(
            String token,
            Set<String> configuredConstants,
            HintVotes votes,
            int weight) {
        if (token == null || token.isBlank()) {
            return;
        }
        addPathTokenVariants(token, configuredConstants, votes, weight);
        if (token.contains(".")) {
            addPathTokenVariants(token.substring(token.lastIndexOf('.') + 1), configuredConstants, votes, weight);
        }
    }

    private static void addPathTokenVariants(
            String token,
            Set<String> configuredConstants,
            HintVotes votes,
            int weight) {
        maybeVoteConstant(votes, normalizeToken(token), configuredConstants, weight);
        for (String part : token.split("[-_.]")) {
            if (!part.isBlank()) {
                maybeVoteConstant(votes, normalizeToken(part), configuredConstants, weight);
            }
        }
        if (token.toUpperCase(Locale.ROOT).contains("_")) {
            String tail = token.substring(token.lastIndexOf('_') + 1);
            maybeVoteConstant(votes, normalizeToken(tail), configuredConstants, weight);
        }
    }

    private static void addEnumReference(
            String reference,
            Set<String> configuredConstants,
            HintVotes votes,
            int weight) {
        if (reference == null || reference.isBlank()) {
            return;
        }
        maybeVoteConstant(votes, constantName(reference), configuredConstants, weight);
        if (reference.contains(".")) {
            maybeVoteConstant(votes, constantName(reference), configuredConstants, weight);
        }
        String upper = reference.toUpperCase(Locale.ROOT);
        if (upper.contains("_")) {
            maybeVoteConstant(votes, upper.substring(upper.lastIndexOf('_') + 1), configuredConstants, weight);
        }
    }

    private static void addUniqueIdentifierMatch(
            String identifier,
            Set<String> configuredConstants,
            HintVotes votes,
            int weight) {
        if (identifier == null || identifier.isBlank()) {
            return;
        }
        String upper = identifier.toUpperCase(Locale.ROOT);
        List<String> matched = new ArrayList<>();
        for (String constant : configuredConstants) {
            if (upper.contains(constant)) {
                matched.add(constant);
            }
        }
        if (matched.size() == 1) {
            votes.add(matched.get(0), weight);
        }
    }

    private static void maybeVoteConstant(
            HintVotes votes,
            String candidate,
            Set<String> configuredConstants,
            int weight) {
        if (candidate == null || candidate.isBlank()) {
            return;
        }
        String normalized = candidate.toUpperCase(Locale.ROOT);
        if (configuredConstants.contains(normalized)) {
            votes.add(normalized, weight);
        }
    }

    private static List<String> filterPolyToConstant(
            List<String> poly,
            String constant,
            String eventTypeFqn,
            CodebaseContext context) {
        List<String> narrowed = new ArrayList<>();
        for (String candidate : poly) {
            if (candidate == null || candidate.startsWith("<SYMBOLIC:") || candidate.startsWith("ENUM_SET:")) {
                continue;
            }
            if (!constant.equalsIgnoreCase(constantName(candidate))) {
                continue;
            }
            String canonical = candidate.contains(".")
                    ? MachineEnumCanonicalizer.canonicalizeLabel(candidate, eventTypeFqn, context)
                    : eventTypeFqn + "." + constant;
            if (!narrowed.contains(canonical)) {
                narrowed.add(canonical);
            }
        }
        return narrowed;
    }

    private static String normalizeToken(String token) {
        return token.replace('-', '_').toUpperCase(Locale.ROOT);
    }

    private static List<String> splitCamelCase(String identifier) {
        if (identifier == null || identifier.isBlank()) {
            return List.of();
        }
        List<String> tokens = new ArrayList<>();
        StringBuilder current = new StringBuilder();
        for (int i = 0; i < identifier.length(); i++) {
            char ch = identifier.charAt(i);
            if (Character.isUpperCase(ch) && !current.isEmpty()) {
                tokens.add(current.toString());
                current.setLength(0);
            }
            current.append(ch);
        }
        if (!current.isEmpty()) {
            tokens.add(current.toString());
        }
        return tokens;
    }

    private static Set<String> configuredEventConstants(
            List<Transition> transitions,
            String eventTypeFqn,
            CodebaseContext context) {
        Set<String> constants = new LinkedHashSet<>();
        for (String event : MachineEnumCanonicalizer.polymorphicEventsFromTransitions(
                transitions, eventTypeFqn, context)) {
            constants.add(constantName(event).toUpperCase(Locale.ROOT));
        }
        return constants;
    }

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

    private static final class HintVotes {
        private final Map<String, Integer> scores = new HashMap<>();
        private final Map<String, Integer> maxWeight = new HashMap<>();

        void add(String constant, int weight) {
            if (constant == null || constant.isBlank()) {
                return;
            }
            String key = constant.toUpperCase(Locale.ROOT);
            scores.merge(key, weight, Integer::sum);
            maxWeight.merge(key, weight, Math::max);
        }

        String choose(Set<String> configured) {
            String strongUnique = uniqueAboveWeight(configured, STRONG);
            if (strongUnique != null) {
                return strongUnique;
            }
            String mediumUnique = uniqueAboveWeight(configured, MEDIUM);
            if (mediumUnique != null) {
                return mediumUnique;
            }
            String best = null;
            int bestScore = 0;
            for (String constant : configured) {
                int score = scores.getOrDefault(constant, 0);
                if (score > bestScore) {
                    bestScore = score;
                    best = constant;
                }
            }
            if (best == null || bestScore < MEDIUM) {
                return null;
            }
            final int winningScore = bestScore;
            List<String> tied = new ArrayList<>();
            for (String constant : configured) {
                if (scores.getOrDefault(constant, 0) == winningScore) {
                    tied.add(constant);
                }
            }
            if (tied.size() == 1) {
                return best;
            }
            String maxWeightWinner = null;
            int bestMaxWeight = 0;
            for (String constant : tied) {
                int weight = maxWeight.getOrDefault(constant, 0);
                if (weight > bestMaxWeight) {
                    bestMaxWeight = weight;
                    maxWeightWinner = constant;
                } else if (weight == bestMaxWeight && weight > 0) {
                    maxWeightWinner = null;
                }
            }
            return maxWeightWinner;
        }

        private String uniqueAboveWeight(Set<String> configured, int minWeight) {
            String chosen = null;
            for (String constant : configured) {
                if (maxWeight.getOrDefault(constant, 0) >= minWeight) {
                    if (chosen != null) {
                        return null;
                    }
                    chosen = constant;
                }
            }
            return chosen;
        }
    }
}
