package click.kamil.springstatemachineexporter.service;

import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
import click.kamil.springstatemachineexporter.analysis.model.BusinessFlow;
import click.kamil.springstatemachineexporter.analysis.service.AnalysisResultFinalizer;
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.analysis.service.ToolingClasspath;
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, EnrichmentService.createDefault());
    }

    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(), null, null, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, true);
    }

    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, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, true);
    }

    public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles, Path flowsOverride, String machineFilter) throws IOException {
        runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, activeProfiles, flowsOverride, machineFilter, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, true);
    }

    public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat) throws IOException {
        runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, activeProfiles, null, null, eventFormat, stateFormat, true);
    }

    public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles, Path flowsOverride, String machineFilter, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat) throws IOException {
        runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, activeProfiles, flowsOverride, machineFilter, eventFormat, stateFormat, true);
    }

    public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles, Path flowsOverride, String machineFilter, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat, boolean includeGeneratedSources) throws IOException {
        runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, activeProfiles, flowsOverride, machineFilter, eventFormat, stateFormat, includeGeneratedSources, true);
    }

    public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles, Path flowsOverride, String machineFilter, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat, boolean includeGeneratedSources, boolean inlineAccessors) throws IOException {
        CodebaseContext context = new CodebaseContext();
        context.setActiveProfiles(activeProfiles);
        context.setInlineAccessors(inlineAccessors);
        
        click.kamil.springstatemachineexporter.analysis.resolver.SiblingDependencyResolver siblingResolver =
                new click.kamil.springstatemachineexporter.analysis.resolver.SiblingDependencyResolver();
        click.kamil.springstatemachineexporter.analysis.resolver.ProjectModuleGraph projectGraph =
                siblingResolver.analyzeProject(inputDir);
        Set<Path> allPaths = projectGraph.resolveScanPaths(inputDir, includeGeneratedSources);
        log.info("Static module graph discovered {} scan roots from {} (includeGeneratedSources={})",
                allPaths.size(), inputDir, includeGeneratedSources);
        
        // 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()
                .flatMap(p -> {
                    List<Path> roots = new ArrayList<>();
                    Path mainJava = p.resolve("src/main/java");
                    if (Files.exists(mainJava)) {
                        roots.add(mainJava);
                    }
                    if (includeGeneratedSources) {
                        roots.addAll(new click.kamil.springstatemachineexporter.analysis.resolver.GeneratedSourceDiscovery()
                                .discoverSourceRoots(p));
                    }
                    return roots.stream();
                })
                .map(Path::toString)
                .distinct()
                .collect(Collectors.toList());
        log.info("Setting JDT sourcepath: {}", sourcepaths);
        context.setSourcepath(sourcepaths);
        List<String> toolingClasspath = ToolingClasspath.currentJvmJarEntries();
        if (!toolingClasspath.isEmpty()) {
            context.setClasspath(toolingClasspath);
            log.info("Using {} tooling JARs for library type bindings (target project build is not executed).", toolingClasspath.size());
        }
        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.debug("Loading optional hints.json override 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(), eventFormat, stateFormat);
    }

    public void runJsonExporter(Path jsonFile, Path outputDir, List<String> selectedFormats) throws IOException {
        runJsonExporter(jsonFile, outputDir, selectedFormats, Collections.emptyList(), click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn);
    }

    public void runJsonExporter(Path jsonFile, Path outputDir, List<String> selectedFormats, List<String> activeProfiles) throws IOException {
        runJsonExporter(jsonFile, outputDir, selectedFormats, activeProfiles, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn);
    }

    public void runJsonExporter(Path jsonFile, Path outputDir, List<String> selectedFormats, List<String> activeProfiles, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat) throws IOException {
        runJsonExporter(jsonFile, outputDir, selectedFormats, activeProfiles, eventFormat, stateFormat, null);
    }

    public void runJsonExporter(Path jsonFile, Path outputDir, List<String> selectedFormats, List<String> activeProfiles, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat, Path optionalSourceDir) throws IOException {
        JsonImportService jsonImportService = new JsonImportService();
        AnalysisResult result = jsonImportService.importAnalysisResult(jsonFile);

        CodebaseContext context = null;
        Path sourceRoot = optionalSourceDir;
        if (sourceRoot == null) {
            sourceRoot = click.kamil.springstatemachineexporter.analysis.service.JsonExportContextFactory
                    .findProjectRoot(jsonFile.toAbsolutePath().getParent());
        }
        if (sourceRoot != null) {
            log.info("JSON re-export: scanning source project at {}", sourceRoot);
            try {
                context = click.kamil.springstatemachineexporter.analysis.service.JsonExportContextFactory
                        .scanProjectRoot(sourceRoot, activeProfiles);
            } catch (RuntimeException ex) {
                log.warn("JSON re-export: source scan failed; using embedded machine types if present", ex);
            }
        } else {
            log.info("JSON re-export: no source project found; using embedded machine types if present");
        }

        resolveProperties(result, activeProfiles, context);

        StateMachineTypeResolver.MachineTypes machineTypes =
                click.kamil.springstatemachineexporter.analysis.service.JsonExportContextFactory.resolveMachineTypes(
                        result.getName(),
                        result.getStateTypeFqn(),
                        result.getEventTypeFqn(),
                        context);
        // Canonicalize transitions/states for relink matching; trigger/callChain canonicalization
        // here is superseded by refreshCallChainsAndRelink / relinkAfterPropertyResolution
        // (TriggerCanonicalizationEnricher runs again before TransitionLinkerEnricher).
        AnalysisResultFinalizer.applyCanonicalization(result, context, machineTypes);
        if (context != null && sourceRoot != null
                && click.kamil.springstatemachineexporter.analysis.service.JsonExportContextFactory
                        .contextContainsAnalysisSources(result, context)
                && result.getMetadata() != null
                && ((result.getMetadata().getEntryPoints() != null
                        && !result.getMetadata().getEntryPoints().isEmpty())
                    || (result.getMetadata().getCallChains() != null
                        && !result.getMetadata().getCallChains().isEmpty()))) {
            JdtIntelligenceProvider intelligence = new JdtIntelligenceProvider(context, sourceRoot);
            enrichmentService.refreshCallChainsAndRelink(result, context, intelligence);
        } else {
            enrichmentService.relinkAfterPropertyResolution(result, context, null);
        }
        AnalysisResultFinalizer.finalizeResult(result, context, machineTypes);

        generateOutputs(outputDir, result, selectedFormats, eventFormat, stateFormat);
    }

    private void resolveProperties(AnalysisResult result, List<String> activeProfiles, CodebaseContext context) {
        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, context);
    }

    private void exportAll(CodebaseContext context, CodebaseIntelligenceProvider intelligence, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<BusinessFlow> flows, String machineFilter, List<String> activeProfiles, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat) 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, eventFormat, stateFormat);
            }
        }

        // 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, eventFormat, stateFormat);
            }
        }
    }

    private void processEntryPoint(TypeDeclaration td, Path outputDir, CodebaseContext context, CodebaseIntelligenceProvider intelligence, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<BusinessFlow> flows, List<String> activeProfiles, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat) 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);

        StateMachineTypeResolver.MachineTypes machineTypes = StateMachineTypeResolver.resolveTypes(className, context);
        MachineEnumCanonicalizer.canonicalizeTransitions(transitions, machineTypes, context);

        aggregator.aggregateStates(td);
        Set<String> initialStatesAst = MachineEnumCanonicalizer.canonicalizeStateLabels(
                aggregator.getInitialStates(), machineTypes.stateTypeFqn(), context);
        Set<String> endStatesAst = MachineEnumCanonicalizer.canonicalizeStateLabels(
                aggregator.getEndStates(), machineTypes.stateTypeFqn(), context);

        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 = MachineEnumCanonicalizer.canonicalizeStates(
                TransitionStateUtils.findAllStates(transitions, initialStatesAst, endStatesAst),
                machineTypes.stateTypeFqn(), context);

        if (allStates.isEmpty() && transitions.isEmpty()) {
            log.info("Skipping empty state machine config: {}", className);
            return;
        }

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

        enrichmentService.enrichPreProperty(result, context, intelligence);
        resolveProperties(result, activeProfiles, context);
        enrichmentService.enrichPostProperty(result, context, intelligence);
        AnalysisResultFinalizer.finalizeResult(result, context);

        generateOutputs(outputDir, result, selectedFormats, eventFormat, stateFormat);
    }

    private void processBeanMethod(MethodDeclaration m, Path outputDir, CodebaseContext context, CodebaseIntelligenceProvider intelligence, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<BusinessFlow> flows, List<String> activeProfiles, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat) 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);

        StateMachineTypeResolver.MachineTypes machineTypes = StateMachineTypeResolver.resolveTypes(parentFqn, context);
        MachineEnumCanonicalizer.canonicalizeTransitions(transitions, machineTypes, context);

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

        if (allStates.isEmpty() && transitions.isEmpty()) {
            log.info("Skipping empty state machine bean: {}", uniqueName);
            return;
        }

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

        enrichmentService.enrichPreProperty(result, context, intelligence);
        resolveProperties(result, activeProfiles, context);
        enrichmentService.enrichPostProperty(result, context, intelligence);
        AnalysisResultFinalizer.finalizeResult(result, context);

        generateOutputs(outputDir, result, selectedFormats, eventFormat, stateFormat);
    }

    private void generateOutputs(Path outputDir, AnalysisResult result, List<String> selectedFormats, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat) 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())
                .eventFormat(eventFormat)
                .stateFormat(stateFormat)
                .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);
            }
        }
    }
}
