package click.kamil.springstatemachineexporter.analysis.enricher;

import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

@Slf4j
public class CallChainEnricher implements AnalysisEnricher {

    @Override
    public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
        log.info("Enriching {} with call chains", result.getName());
        List<CallChain> chains = buildCallChainsForMachine(result, context, intelligence, true);
        if (chains == null) {
            return;
        }
        result.addMetadata(CodebaseMetadata.builder()
                .callChains(chains)
                .build());
    }

    /**
     * Re-resolves call chains from the codebase call graph for the given machine.
     * Used on JSON re-export when source is available to refresh {@code polymorphicEvents},
     * {@code external}, and branch {@code constraint} on trigger points.
     */
    public static List<CallChain> buildCallChainsForMachine(
            AnalysisResult result,
            CodebaseContext context,
            CodebaseIntelligenceProvider intelligence) {
        return buildCallChainsForMachine(result, context, intelligence, false);
    }

    public static List<CallChain> buildCallChainsForMachine(
            AnalysisResult result,
            CodebaseContext context,
            CodebaseIntelligenceProvider intelligence,
            boolean preferMetadataTriggers) {
        if (result.getMetadata() == null || intelligence == null) {
            return null;
        }
        List<EntryPoint> entryPoints = result.getMetadata().getEntryPoints();
        if (entryPoints == null || entryPoints.isEmpty()) {
            entryPoints = synthesizeEntryPoints(result.getMetadata().getCallChains());
        }
        if (entryPoints == null || entryPoints.isEmpty()) {
            return null;
        }

        List<TriggerPoint> scopedTriggers = null;
        if (preferMetadataTriggers) {
            scopedTriggers = result.getMetadata().getTriggers();
        }
        if (scopedTriggers == null || scopedTriggers.isEmpty()) {
            scopedTriggers = MachineScopeFilter.filterTriggersForMachine(
                    intelligence.findTriggerPoints(),
                    result.getName(),
                    context);
        }

        List<CallChain> chains = intelligence.findCallChains(entryPoints, scopedTriggers);
        // Semantic deduplication runs in TransitionLinkerEnricher after transitions are linked.
        return MachineScopeFilter.filterCallChainsForMachine(chains, result.getName(), context);
    }

    private static List<EntryPoint> synthesizeEntryPoints(List<CallChain> callChains) {
        if (callChains == null || callChains.isEmpty()) {
            return List.of();
        }
        Map<String, EntryPoint> byMethod = new LinkedHashMap<>();
        for (CallChain chain : callChains) {
            EntryPoint entryPoint = chain.getEntryPoint();
            if (hasClassAndMethod(entryPoint)) {
                byMethod.putIfAbsent(entryPoint.getClassName() + "#" + entryPoint.getMethodName(), entryPoint);
                continue;
            }
            TriggerPoint triggerPoint = chain.getTriggerPoint();
            if (triggerPoint != null
                    && triggerPoint.getClassName() != null
                    && triggerPoint.getMethodName() != null) {
                EntryPoint fallback = EntryPoint.builder()
                        .type(EntryPoint.Type.CUSTOM)
                        .className(triggerPoint.getClassName())
                        .methodName(triggerPoint.getMethodName())
                        .sourceFile(triggerPoint.getSourceFile())
                        .build();
                byMethod.putIfAbsent(fallback.getClassName() + "#" + fallback.getMethodName(), fallback);
            }
        }
        return new ArrayList<>(byMethod.values());
    }

    private static boolean hasClassAndMethod(EntryPoint entryPoint) {
        return entryPoint != null
                && entryPoint.getClassName() != null
                && entryPoint.getMethodName() != null;
    }
}
