package click.kamil.springstatemachineexporter.analysis.resolver;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Queue;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Static wiring model built from parsed {@code settings.gradle}, {@code build.gradle}, and {@code pom.xml}
 * files — no Maven/Gradle execution.
 */
public final class ProjectModuleGraph {

    public enum BuildSystem {
        GRADLE,
        MAVEN,
        MIXED
    }

    public enum DependencyKind {
        LOCAL_PROJECT,
        EXTERNAL_LIBRARY
    }

    public record MavenCoordinates(String groupId, String artifactId) {
        public MavenCoordinates {
            Objects.requireNonNull(groupId, "groupId");
            Objects.requireNonNull(artifactId, "artifactId");
        }

        public String key() {
            return groupId + ":" + artifactId;
        }
    }

    public record ProjectModule(
            String id,
            Path directory,
            BuildSystem buildSystem,
            MavenCoordinates mavenCoordinates) {

        public Optional<MavenCoordinates> coordinates() {
            return Optional.ofNullable(mavenCoordinates);
        }
    }

    public record ModuleDependency(
            String fromModuleId,
            String toModuleId,
            MavenCoordinates externalCoordinates,
            DependencyKind kind) {
    }

    private final Path workspaceRoot;
    private final Map<String, ProjectModule> modulesById;
    private final Map<MavenCoordinates, ProjectModule> modulesByCoordinates;
    private final List<ModuleDependency> dependencies;

    public ProjectModuleGraph(
            Path workspaceRoot,
            Map<String, ProjectModule> modulesById,
            Map<MavenCoordinates, ProjectModule> modulesByCoordinates,
            List<ModuleDependency> dependencies) {
        this.workspaceRoot = workspaceRoot != null ? workspaceRoot.normalize() : null;
        this.modulesById = Map.copyOf(modulesById);
        this.modulesByCoordinates = Map.copyOf(modulesByCoordinates);
        this.dependencies = List.copyOf(dependencies);
    }

    public Path workspaceRoot() {
        return workspaceRoot;
    }

    public Map<String, ProjectModule> modulesById() {
        return modulesById;
    }

    public Map<MavenCoordinates, ProjectModule> modulesByCoordinates() {
        return modulesByCoordinates;
    }

    public List<ModuleDependency> dependencies() {
        return dependencies;
    }

    public Optional<ProjectModule> findByDirectory(Path directory) {
        if (directory == null) {
            return Optional.empty();
        }
        Path normalized = directory.toAbsolutePath().normalize();
        return modulesById.values().stream()
                .filter(module -> module.directory().toAbsolutePath().normalize().equals(normalized))
                .findFirst();
    }

    public Optional<ProjectModule> findContainingModule(Path path) {
        if (path == null) {
            return Optional.empty();
        }
        Path normalized = path.toAbsolutePath().normalize();
        ProjectModule best = null;
        int bestLength = -1;
        for (ProjectModule module : modulesById.values()) {
            Path moduleDir = module.directory().toAbsolutePath().normalize();
            if (normalized.startsWith(moduleDir) && moduleDir.getNameCount() > bestLength) {
                best = module;
                bestLength = moduleDir.getNameCount();
            }
        }
        return Optional.ofNullable(best);
    }

    public Set<Path> allModuleDirectories() {
        Set<Path> paths = new LinkedHashSet<>();
        for (ProjectModule module : modulesById.values()) {
            if (Files.isDirectory(module.directory())) {
                paths.add(module.directory().toAbsolutePath().normalize());
            }
        }
        return paths;
    }

    public boolean isGradleAggregatorRoot(Path directory) {
        if (directory == null || workspaceRoot == null) {
            return false;
        }
        return directory.toAbsolutePath().normalize().equals(workspaceRoot)
                && modulesById.values().stream().anyMatch(module -> module.buildSystem() == BuildSystem.GRADLE);
    }

    public boolean isMavenAggregatorRoot(Path directory) {
        if (directory == null) {
            return false;
        }
        Path normalized = directory.toAbsolutePath().normalize();
        long childModules = modulesById.values().stream()
                .filter(module -> module.directory().toAbsolutePath().normalize().startsWith(normalized))
                .filter(module -> !module.directory().toAbsolutePath().normalize().equals(normalized))
                .count();
        return childModules > 0 && findByDirectory(normalized)
                .map(module -> module.buildSystem() == BuildSystem.MAVEN || module.buildSystem() == BuildSystem.MIXED)
                .orElse(false);
    }

    /**
     * Resolves scan roots for analysis: the input directory, all Gradle includes when scanning from the
     * workspace root, and transitively reachable local modules from build-file wiring.
     */
    public Set<Path> resolveScanPaths(Path inputDir) {
        return resolveScanPaths(inputDir, true);
    }

    public Set<Path> resolveScanPaths(Path inputDir, boolean includeGeneratedSources) {
        if (inputDir == null) {
            return Set.of();
        }
        Set<Path> scanPaths = new LinkedHashSet<>();
        Path normalizedInput = inputDir.toAbsolutePath().normalize();
        scanPaths.add(normalizedInput);

        if (workspaceRoot != null && normalizedInput.equals(workspaceRoot)) {
            scanPaths.addAll(allModuleDirectories());
        }

        Optional<ProjectModule> inputModule = findContainingModule(normalizedInput);
        if (inputModule.isPresent()) {
            scanPaths.addAll(transitiveLocalModuleDirectories(inputModule.get().id()));
            resolveMavenParentDirectory(inputModule.get().directory()).ifPresent(scanPaths::add);
            Path parentDir = inputModule.get().directory().getParent();
            if (parentDir != null && Files.exists(parentDir.resolve("pom.xml"))) {
                scanPaths.add(parentDir.toAbsolutePath().normalize());
            }
        }

        if (includeGeneratedSources) {
            scanPaths.addAll(new GeneratedSourceDiscovery().discoverSourceRoots(scanPaths));
        }
        return scanPaths;
    }

    static Optional<Path> resolveMavenParentDirectory(Path moduleDirectory) {
        if (moduleDirectory == null) {
            return Optional.empty();
        }
        Path pom = moduleDirectory.resolve("pom.xml");
        if (!Files.exists(pom)) {
            return Optional.empty();
        }
        try {
            String content = Files.readString(pom);
            Matcher parentMatcher = Pattern.compile(
                    "<parent>.*?<relativePath>(.*?)</relativePath>.*?</parent>", Pattern.DOTALL).matcher(content);
            if (!parentMatcher.find()) {
                return Optional.empty();
            }
            String relPathStr = parentMatcher.group(1).trim();
            Path relPath = moduleDirectory.resolve(relPathStr).normalize();
            if (Files.isRegularFile(relPath) && "pom.xml".equals(relPath.getFileName().toString())) {
                return Optional.of(relPath.getParent().toAbsolutePath().normalize());
            }
            if (Files.isDirectory(relPath)) {
                return Optional.of(relPath.toAbsolutePath().normalize());
            }
        } catch (IOException ignored) {
            return Optional.empty();
        }
        return Optional.empty();
    }

    public Set<Path> transitiveLocalModuleDirectories(String rootModuleId) {
        Set<Path> reachable = new LinkedHashSet<>();
        Queue<String> queue = new ArrayDeque<>();
        Set<String> visited = new HashSet<>();
        if (rootModuleId != null) {
            queue.add(rootModuleId);
        }
        while (!queue.isEmpty()) {
            String current = queue.poll();
            if (!visited.add(current)) {
                continue;
            }
            ProjectModule module = modulesById.get(current);
            if (module != null && Files.isDirectory(module.directory())) {
                reachable.add(module.directory().toAbsolutePath().normalize());
            }
            for (ModuleDependency dependency : dependencies) {
                if (dependency.kind() == DependencyKind.LOCAL_PROJECT
                        && current.equals(dependency.fromModuleId())
                        && dependency.toModuleId() != null) {
                    queue.add(dependency.toModuleId());
                }
            }
        }
        return reachable;
    }

    public List<ModuleDependency> localDependenciesOf(String moduleId) {
        List<ModuleDependency> result = new ArrayList<>();
        for (ModuleDependency dependency : dependencies) {
            if (moduleId.equals(dependency.fromModuleId())) {
                result.add(dependency);
            }
        }
        return result;
    }
}
