package click.kamil.springstatemachineexporter.analysis.enricher;

import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.EntryPointKeys;
import click.kamil.springstatemachineexporter.analysis.model.LinkResolution;
import click.kamil.springstatemachineexporter.analysis.model.MatchedTransition;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * Collapses call chains that represent the same logical entrypoint-to-transition link
 * but were discovered via different call-graph paths.
 */
public final class SemanticCallChainDedup {

    public enum KeepStrategy {
        SHORTEST,
        FIRST
    }

    private SemanticCallChainDedup() {
    }

    public static List<CallChain> deduplicate(List<CallChain> chains) {
        return deduplicate(chains, KeepStrategy.SHORTEST);
    }

    public static List<CallChain> deduplicate(List<CallChain> chains, KeepStrategy strategy) {
        if (chains == null || chains.isEmpty()) {
            return chains == null ? List.of() : chains;
        }
        Map<String, CallChain> byKey = new LinkedHashMap<>();
        for (CallChain chain : chains) {
            if (chain == null) {
                continue;
            }
            String key = semanticKey(chain);
            CallChain existing = byKey.get(key);
            if (existing == null) {
                byKey.put(key, chain);
            } else {
                byKey.put(key, pickPreferred(existing, chain, strategy));
            }
        }
        return new ArrayList<>(byKey.values());
    }

    public static List<CallChain> merge(
            List<CallChain> existing,
            List<CallChain> refreshed,
            KeepStrategy strategy) {
        List<CallChain> combined = new ArrayList<>();
        if (existing != null) {
            combined.addAll(existing);
        }
        if (refreshed != null) {
            combined.addAll(refreshed);
        }
        return deduplicate(combined, strategy);
    }

    /**
     * JSON refresh path: when refresh produces any chains for an entry point, those replace all
     * existing chains for that entry (so unresolved stubs like {@code event} do not linger).
     * Multiple refreshed chains per entry (multi-event fan-out) are preserved, then semantic
     * dedup collapses path variants.
     */
    public static List<CallChain> mergeRefresh(
            List<CallChain> existing,
            List<CallChain> refreshed,
            KeepStrategy strategy) {
        Map<String, List<CallChain>> existingByEntry = groupByEntryPoint(existing);
        Map<String, List<CallChain>> refreshedByEntry = groupByEntryPoint(refreshed);
        LinkedHashSet<String> entries = new LinkedHashSet<>();
        entries.addAll(existingByEntry.keySet());
        entries.addAll(refreshedByEntry.keySet());

        List<CallChain> combined = new ArrayList<>();
        for (String entryKey : entries) {
            if (refreshedByEntry.containsKey(entryKey)) {
                combined.addAll(refreshedByEntry.get(entryKey));
            } else {
                combined.addAll(existingByEntry.getOrDefault(entryKey, List.of()));
            }
        }
        return deduplicate(combined, strategy);
    }

    private static Map<String, List<CallChain>> groupByEntryPoint(List<CallChain> chains) {
        Map<String, List<CallChain>> byEntry = new LinkedHashMap<>();
        if (chains == null) {
            return byEntry;
        }
        for (CallChain chain : chains) {
            if (chain == null) {
                continue;
            }
            byEntry.computeIfAbsent(EntryPointKeys.of(chain), key -> new ArrayList<>()).add(chain);
        }
        return byEntry;
    }

    static String semanticKey(CallChain chain) {
        StringBuilder key = new StringBuilder();
        key.append(EntryPointKeys.of(chain)).append('|');
        key.append(triggerKey(chain.getTriggerPoint())).append('|');
        key.append(linkResolutionKey(chain.getLinkResolution())).append('|');
        key.append(matchedTransitionsKey(chain.getMatchedTransitions())).append('|');
        key.append(nullToEmpty(chain.getContextMachineId()));
        return key.toString();
    }

    private static CallChain pickPreferred(CallChain existing, CallChain candidate, KeepStrategy strategy) {
        if (strategy == KeepStrategy.FIRST) {
            return existing;
        }
        boolean existingAnchored = startsAtEntryPoint(existing);
        boolean candidateAnchored = startsAtEntryPoint(candidate);
        if (candidateAnchored != existingAnchored) {
            return candidateAnchored ? candidate : existing;
        }
        int existingLen = chainLength(existing);
        int candidateLen = chainLength(candidate);
        if (candidateLen < existingLen) {
            return candidate;
        }
        if (candidateLen > existingLen) {
            return existing;
        }
        int existingRichness = metadataRichness(existing);
        int candidateRichness = metadataRichness(candidate);
        if (candidateRichness != existingRichness) {
            return candidateRichness > existingRichness ? candidate : existing;
        }
        List<String> existingChain = existing.getMethodChain();
        List<String> candidateChain = candidate.getMethodChain();
        if (existingChain == null) {
            return candidate;
        }
        if (candidateChain == null) {
            return existing;
        }
        return candidateChain.toString().compareTo(existingChain.toString()) < 0 ? candidate : existing;
    }

    /**
     * True when the first hop equals the entry point method identity
     * ({@code className.methodName} or {@code className#methodName} key form).
     */
    static boolean startsAtEntryPoint(CallChain chain) {
        if (chain == null) {
            return false;
        }
        EntryPoint entryPoint = chain.getEntryPoint();
        List<String> methodChain = chain.getMethodChain();
        if (entryPoint == null
                || entryPoint.getClassName() == null
                || entryPoint.getMethodName() == null
                || methodChain == null
                || methodChain.isEmpty()) {
            return false;
        }
        String firstHop = methodChain.get(0);
        if (firstHop == null || firstHop.isBlank()) {
            return false;
        }
        String entryHop = entryPoint.getClassName() + "." + entryPoint.getMethodName();
        if (entryHop.equals(firstHop)) {
            return true;
        }
        return EntryPointKeys.normalizeMethodHop(firstHop)
                .equals(entryPoint.getClassName() + "#" + entryPoint.getMethodName());
    }

    private static int metadataRichness(CallChain chain) {
        int score = 0;
        if (chain.getEntryPoint() != null) {
            score += 2;
        }
        if (chain.getMethodChain() != null && !chain.getMethodChain().isEmpty()) {
            score += 1;
        }
        if (chain.getMatchedTransitions() != null && !chain.getMatchedTransitions().isEmpty()) {
            score += 4;
        }
        return score;
    }

    private static int chainLength(CallChain chain) {
        return chain.getMethodChain() == null ? Integer.MAX_VALUE : chain.getMethodChain().size();
    }

    private static String triggerKey(TriggerPoint trigger) {
        if (trigger == null) {
            return "";
        }
        StringBuilder key = new StringBuilder();
        key.append(nullToEmpty(trigger.getClassName())).append('#');
        key.append(nullToEmpty(trigger.getMethodName())).append('|');
        key.append(nullToEmpty(trigger.getEvent())).append('|');
        key.append(nullToEmpty(trigger.getConstraint())).append('|');
        key.append(trigger.isExternal()).append('|');
        key.append(trigger.isAmbiguous()).append('|');
        key.append(nullToEmpty(trigger.getSourceState()));
        return key.toString();
    }

    private static String linkResolutionKey(LinkResolution resolution) {
        return resolution == null ? "" : resolution.name();
    }

    private static String matchedTransitionsKey(List<MatchedTransition> matchedTransitions) {
        if (matchedTransitions == null || matchedTransitions.isEmpty()) {
            return "";
        }
        return matchedTransitions.stream()
                .filter(Objects::nonNull)
                .sorted(Comparator
                        .comparing((MatchedTransition mt) -> nullToEmpty(mt.getEvent()))
                        .thenComparing(mt -> nullToEmpty(mt.getSourceState()))
                        .thenComparing(mt -> nullToEmpty(mt.getTargetState())))
                .map(mt -> nullToEmpty(mt.getEvent()) + ">"
                        + nullToEmpty(mt.getSourceState()) + ">"
                        + nullToEmpty(mt.getTargetState()))
                .collect(Collectors.joining(","));
    }

    private static String nullToEmpty(String value) {
        return value == null ? "" : value;
    }
}
