package click.kamil.springstatemachineexporter.service;

import click.kamil.springstatemachineexporter.analysis.enricher.CallChainEnricher;
import click.kamil.springstatemachineexporter.analysis.enricher.EntryPointEnricher;
import click.kamil.springstatemachineexporter.analysis.enricher.PropertyEnricher;
import click.kamil.springstatemachineexporter.analysis.enricher.TriggerEnricher;
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
import click.kamil.springstatemachineexporter.analysis.model.BusinessFlow;
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
import click.kamil.springstatemachineexporter.analysis.service.EnrichmentService;
import click.kamil.springstatemachineexporter.analysis.service.JdtIntelligenceProvider;
import click.kamil.springstatemachineexporter.ast.app.AstTransitionParser;
import click.kamil.springstatemachineexporter.ast.app.StateMachineAggregator;
import click.kamil.springstatemachineexporter.ast.app.TransitionStateUtils;
import click.kamil.springstatemachineexporter.model.StateMachineModel;
import click.kamil.springstatemachineexporter.model.Transition;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import click.kamil.springstatemachineexporter.exporter.ExportOptions;
import click.kamil.springstatemachineexporter.exporter.StateMachineExporter;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.TypeDeclaration;

import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Collectors;

@Slf4j
public class ExportService {

    private final List<StateMachineExporter> exporters;
    private final EnrichmentService enrichmentService;

    public ExportService(List<StateMachineExporter> exporters, EnrichmentService enrichmentService) {
        this.exporters = exporters;
        this.enrichmentService = enrichmentService;
    }

    public ExportService(List<StateMachineExporter> exporters) {
        this(exporters, new EnrichmentService(List.of(
                new TriggerEnricher(),
                new EntryPointEnricher(),
                new PropertyEnricher(),
                new CallChainEnricher()
        )));
    }

    private static final List<String> TARGET_ANNOTATIONS = List.of("EnableStateMachineFactory", "EnableStateMachine");
    public static final Set<String> STATE_MACHINE_RETURN_TYPES = Set.of("StateMachine", "StateMachineFactory", "StateMachineModelFactory");

    public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds) throws IOException {
        runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, Collections.emptyList());
    }

    public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles) throws IOException {
        runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, activeProfiles, null, null);
    }

    public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles, Path flowsOverride, String machineFilter) throws IOException {
        CodebaseContext context = new CodebaseContext();
        context.setActiveProfiles(activeProfiles);
        
        click.kamil.springstatemachineexporter.analysis.resolver.SiblingDependencyResolver siblingResolver = new click.kamil.springstatemachineexporter.analysis.resolver.SiblingDependencyResolver();
        Set<Path> allPaths = new HashSet<>();
        allPaths.add(inputDir);
        allPaths.addAll(siblingResolver.resolveSiblings(inputDir));
        
        // Find project root (common ancestor)
        Path projectRoot = inputDir.toAbsolutePath();
        for (Path p : allPaths) {
            Path ap = p.toAbsolutePath();
            while (projectRoot != null && !ap.startsWith(projectRoot)) {
                projectRoot = projectRoot.getParent();
            }
        }
        context.setProjectRoot(projectRoot);
        log.info("Project root resolved to: {}", projectRoot);
        
        List<String> sourcepaths = allPaths.stream()
                .map(p -> p.resolve("src/main/java").toString())
                .filter(p -> Files.exists(Path.of(p)))
                .collect(Collectors.toList());
        log.info("Setting JDT sourcepath: {}", sourcepaths);
        context.setSourcepath(sourcepaths);
        context.setResolveBindings(true);
        
        Path hintsFile = inputDir.resolve("hints.json");
        if (!Files.exists(hintsFile)) {
            hintsFile = inputDir.resolve("src/main/resources/hints.json");
        }
        
        if (Files.exists(hintsFile)) {
            log.info("Loading hints from {}", hintsFile.toAbsolutePath());
            context.loadLibraryHints(hintsFile);
        }

        List<BusinessFlow> flows = new ArrayList<>();
        Path flowsFile = flowsOverride != null ? flowsOverride : inputDir.resolve("flows.json");
        if (flowsOverride == null && !Files.exists(flowsFile)) {
            flowsFile = inputDir.resolve("src/main/resources/flows.json");
        }
        
        if (Files.exists(flowsFile)) {
            log.info("Loading business flows from {}", flowsFile.toAbsolutePath());
            try {
                flows = new com.fasterxml.jackson.databind.ObjectMapper()
                        .readValue(flowsFile.toFile(), new com.fasterxml.jackson.core.type.TypeReference<List<BusinessFlow>>() {});
            } catch (Exception e) {
                log.error("Failed to load flows.json", e);
            }
        }

        context.scan(allPaths, Collections.emptySet());
        
        CodebaseIntelligenceProvider intelligence = new JdtIntelligenceProvider(context, inputDir);
        exportAll(context, intelligence, outputDir, selectedFormats, renderChoicesAsDiamonds, flows, machineFilter, activeProfiles != null ? activeProfiles : Collections.emptyList());
    }

    public void runJsonExporter(Path jsonFile, Path outputDir, List<String> selectedFormats) throws IOException {
        runJsonExporter(jsonFile, outputDir, selectedFormats, Collections.emptyList());
    }

    public void runJsonExporter(Path jsonFile, Path outputDir, List<String> selectedFormats, List<String> activeProfiles) throws IOException {
        JsonImportService jsonImportService = new JsonImportService();
        AnalysisResult result = jsonImportService.importAnalysisResult(jsonFile);
        
        // Apply property resolution pass if placeholders exist
        resolveProperties(result, activeProfiles);
        
        generateOutputs(outputDir, result, selectedFormats);
    }

    private void resolveProperties(AnalysisResult result, List<String> activeProfiles) {
        Map<String, Map<String, String>> allProps = result.getMetadata().getProperties();
        if (allProps == null || allProps.isEmpty()) return;

        // 1. Merge properties based on active profiles
        Map<String, String> merged = new HashMap<>();
        // Start with default
        if (allProps.containsKey("default")) {
            merged.putAll(allProps.get("default"));
        }
        // Overlay active profiles
        for (String profile : activeProfiles) {
            if (allProps.containsKey(profile)) {
                merged.putAll(allProps.get(profile));
            }
        }

        // 2. Delegate to result for resolution
        result.applyResolution(merged);
    }

    private void exportAll(CodebaseContext context, CodebaseIntelligenceProvider intelligence, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<BusinessFlow> flows, String machineFilter, List<String> activeProfiles) throws IOException {
        Set<String> processedLocations = new HashSet<>();

        // 1. Find entry point classes (annotated or extending adapter)
        List<TypeDeclaration> entryPoints = context.findEntryPointClasses(TARGET_ANNOTATIONS);
        for (TypeDeclaration td : entryPoints) {
            String fqn = context.getFqn(td);
            if (machineFilter != null && !fqn.contains(machineFilter)) continue;
            if (processedLocations.add(fqn)) {
                processEntryPoint(td, outputDir, context, intelligence, selectedFormats, renderChoicesAsDiamonds, flows, activeProfiles);
            }
        }

        // 2. Find methods returning StateMachine beans
        List<MethodDeclaration> beanMethods = context.findBeanMethodsReturning(STATE_MACHINE_RETURN_TYPES);
        for (MethodDeclaration m : beanMethods) {
            TypeDeclaration parentClass = (TypeDeclaration) m.getParent();
            String parentFqn = context.getFqn(parentClass);
            String methodName = m.getName().getIdentifier();
            String uniqueId = parentFqn + "#" + methodName;

            if (machineFilter != null && !uniqueId.contains(machineFilter)) continue;
            if (processedLocations.add(uniqueId)) {
                processBeanMethod(m, outputDir, context, intelligence, selectedFormats, renderChoicesAsDiamonds, flows, activeProfiles);
            }
        }
    }

    private void processEntryPoint(TypeDeclaration td, Path outputDir, CodebaseContext context, CodebaseIntelligenceProvider intelligence, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<BusinessFlow> flows, List<String> activeProfiles) throws IOException {
        String className = context.getFqn(td);
        log.info("Processing state machine config: {}", className);

        StateMachineAggregator aggregator = new StateMachineAggregator(context);
        List<Transition> transitions = aggregator.aggregateTransitions(td);

        aggregator.aggregateStates(td);
        Set<String> initialStatesAst = aggregator.getInitialStates();
        Set<String> endStatesAst = aggregator.getEndStates();

        log.debug("Start States Ast: {}", initialStatesAst);
        log.debug("End States Ast: {}", endStatesAst);

        Set<String> startStates = TransitionStateUtils.findStartStates(transitions, initialStatesAst);
        Set<String> endStates = TransitionStateUtils.findEndStates(transitions, endStatesAst);
        Set<click.kamil.springstatemachineexporter.model.State> allStates = TransitionStateUtils.findAllStates(transitions, initialStatesAst, endStatesAst);

        AnalysisResult result = AnalysisResult.builder()
                .name(className)
                .states(allStates)
                .transitions(transitions)
                .startStates(startStates)
                .endStates(endStates)
                .renderChoicesAsDiamonds(renderChoicesAsDiamonds)
                .flows(flows)
                .build();

        enrichmentService.enrich(result, context, intelligence);

        resolveProperties(result, activeProfiles);

        generateOutputs(outputDir, result, selectedFormats);
    }

    private void processBeanMethod(MethodDeclaration m, Path outputDir, CodebaseContext context, CodebaseIntelligenceProvider intelligence, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<BusinessFlow> flows, List<String> activeProfiles) throws IOException {
        TypeDeclaration parentClass = (TypeDeclaration) m.getParent();
        String parentFqn = context.getFqn(parentClass);
        String methodName = m.getName().getIdentifier();
        String uniqueName = parentFqn + "#" + methodName;

        log.info("Processing state machine bean: {}", uniqueName);

        List<Transition> transitions = AstTransitionParser.parseTransitions(m, context);

        Set<String> startStates = TransitionStateUtils.findStartStates(transitions, null);
        Set<String> endStates = TransitionStateUtils.findEndStates(transitions, null);
        Set<click.kamil.springstatemachineexporter.model.State> allStates = TransitionStateUtils.findAllStates(transitions, null, null);

        AnalysisResult result = AnalysisResult.builder()
                .name(uniqueName)
                .states(allStates)
                .transitions(transitions)
                .startStates(startStates)
                .endStates(endStates)
                .renderChoicesAsDiamonds(renderChoicesAsDiamonds)
                .flows(flows)
                .build();

        enrichmentService.enrich(result, context, intelligence);

        resolveProperties(result, activeProfiles);

        generateOutputs(outputDir, result, selectedFormats);
    }

    private void generateOutputs(Path outputDir, AnalysisResult result, List<String> selectedFormats) throws IOException {
        if (selectedFormats == null || selectedFormats.isEmpty()) {
            return;
        }

        String actualName = result.getName();
        Path targetDir = outputDir.resolve(actualName);
        Files.createDirectories(targetDir);

        ExportOptions options = ExportOptions.builder()
                .renderChoicesAsDiamonds(result.isRenderChoicesAsDiamonds())
                .build();

        for (StateMachineExporter exporter : exporters) {
            String ext = exporter.getFileExtension().toLowerCase();
            if (!selectedFormats.stream().anyMatch(fmt -> ext.contains(fmt.toLowerCase()))) {
                continue;
            }

            String content = exporter.export(result, options);
            String fileName = result.getName() + exporter.getFileExtension();

            try (PrintWriter out = new PrintWriter(targetDir.resolve(fileName).toFile())) {
                out.println(content);
            }
        }
    }
}
