package click.kamil.springstatemachineexporter.analysis.resolver;

import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * Parses Maven/Gradle build files statically to discover modules and local dependency wiring.
 */
@Slf4j
public class StaticBuildFileAnalyzer {

    private static final Pattern GRADLE_PROJECT_DEP = Pattern.compile("project\\(['\"]:(.*?)['\"]\\)");
    private static final Pattern QUOTED_TOKEN = Pattern.compile("['\"]([^'\"]+)['\"]");
    private static final Pattern GRADLE_PROJECT_DIR = Pattern.compile(
            "project\\(['\"]:(.*?)['\"]\\)\\.projectDir\\s*=\\s*(?:file\\(['\"](.*?)['\"]\\)|new File\\(.*?,\\s*['\"](.*?)['\"]\\))");
    private static final Pattern MAVEN_MODULE = Pattern.compile("<module>(.*?)</module>");
    private static final Pattern MAVEN_DEPENDENCY_BLOCK = Pattern.compile("<dependency>(.*?)</dependency>", Pattern.DOTALL);
    private static final Pattern MAVEN_TAG = Pattern.compile("<(groupId|artifactId)>(.*?)</\\1>");

    public ProjectModuleGraph analyze(Path inputDir) throws IOException {
        Path workspaceRoot = findWorkspaceRoot(inputDir);
        Path indexRoot = findIndexingRoot(inputDir, workspaceRoot);
        if (workspaceRoot == null) {
            workspaceRoot = inputDir.toAbsolutePath().normalize();
        }
        if (indexRoot == null) {
            indexRoot = workspaceRoot;
        }

        Map<String, ProjectModuleGraph.ProjectModule> modulesById = new LinkedHashMap<>();
        Map<ProjectModuleGraph.MavenCoordinates, ProjectModuleGraph.ProjectModule> modulesByCoordinates = new LinkedHashMap<>();
        List<ProjectModuleGraph.ModuleDependency> dependencies = new ArrayList<>();

        Path gradleSettingsRoot = Files.exists(workspaceRoot.resolve("settings.gradle"))
                || Files.exists(workspaceRoot.resolve("settings.gradle.kts"))
                ? workspaceRoot
                : indexRoot;
        Map<String, Path> gradleModulePaths = parseGradleIncludes(gradleSettingsRoot);
        for (Map.Entry<String, Path> entry : gradleModulePaths.entrySet()) {
            registerGradleModule(entry.getKey(), entry.getValue(), modulesById);
        }

        List<Path> pomFiles = findPomFiles(indexRoot);
        for (Path pom : pomFiles) {
            registerMavenSubmodules(pom, modulesById, modulesByCoordinates);
        }
        for (Path pom : pomFiles) {
            registerMavenModule(pom, modulesById, modulesByCoordinates);
        }

        ensureInputModule(inputDir, modulesById);

        for (ProjectModuleGraph.ProjectModule module : List.copyOf(modulesById.values())) {
            if (module.buildSystem() == ProjectModuleGraph.BuildSystem.GRADLE
                    || module.buildSystem() == ProjectModuleGraph.BuildSystem.MIXED) {
                dependencies.addAll(parseGradleDependencies(module, modulesById, gradleModulePaths));
            }
        }
        for (Path pom : pomFiles) {
            dependencies.addAll(parseMavenDependencies(pom, modulesById, modulesByCoordinates));
        }

        log.info("Static project graph: {} modules, {} dependencies under {}", modulesById.size(), dependencies.size(), workspaceRoot);
        return new ProjectModuleGraph(workspaceRoot, modulesById, modulesByCoordinates, dependencies);
    }

    Map<String, Path> parseGradleIncludes(Path gradleRoot) throws IOException {
        Map<String, Path> includes = new LinkedHashMap<>();
        if (gradleRoot == null) {
            return includes;
        }
        Path settingsFile = gradleRoot.resolve("settings.gradle");
        if (!Files.exists(settingsFile)) {
            settingsFile = gradleRoot.resolve("settings.gradle.kts");
        }
        if (!Files.exists(settingsFile)) {
            return includes;
        }

        String content = Files.readString(settingsFile);
        for (String includePath : extractGradleIncludePaths(content)) {
            String normalized = normalizeGradlePath(includePath);
            Path moduleDir = resolveGradleModuleDirectory(gradleRoot, normalized, content);
            if (Files.isDirectory(moduleDir)) {
                includes.put(normalized, moduleDir.normalize());
            }
        }
        return includes;
    }

    static Set<String> extractGradleIncludePaths(String settingsContent) {
        Set<String> paths = new LinkedHashSet<>();
        for (String line : settingsContent.split("\\R")) {
            if (!line.contains("include")) {
                continue;
            }
            int includeIndex = line.indexOf("include");
            Matcher tokenMatcher = QUOTED_TOKEN.matcher(line);
            while (tokenMatcher.find()) {
                if (tokenMatcher.start() > includeIndex) {
                    paths.add(normalizeGradlePath(tokenMatcher.group(1)));
                }
            }
        }
        return paths;
    }

    private static String normalizeGradlePath(String includePath) {
        String path = includePath.trim();
        if (path.startsWith(":")) {
            path = path.substring(1);
        }
        return path;
    }

    private Path resolveGradleModuleDirectory(Path gradleRoot, String gradlePath, String settingsContent) {
        String lookup = gradlePath.contains(":") ? gradlePath : gradlePath.replace('/', ':');
        Matcher matcher = GRADLE_PROJECT_DIR.matcher(settingsContent);
        while (matcher.find()) {
            String configuredPath = matcher.group(1);
            if (configuredPath != null && configuredPath.equals(lookup)) {
                String custom = matcher.group(2) != null ? matcher.group(2) : matcher.group(3);
                if (custom != null) {
                    return gradleRoot.resolve(custom).normalize();
                }
            }
        }
        return gradleRoot.resolve(gradlePath.replace(':', '/')).normalize();
    }

    private void registerGradleModule(String gradlePath, Path directory, Map<String, ProjectModuleGraph.ProjectModule> modulesById) {
        String id = "gradle:" + gradlePath;
        ProjectModuleGraph.ProjectModule existing = modulesById.get(id);
        if (existing != null) {
            modulesById.put(id, new ProjectModuleGraph.ProjectModule(
                    id,
                    directory,
                    ProjectModuleGraph.BuildSystem.MIXED,
                    existing.mavenCoordinates()));
            return;
        }
        modulesById.put(id, new ProjectModuleGraph.ProjectModule(
                id,
                directory,
                ProjectModuleGraph.BuildSystem.GRADLE,
                null));
    }

    private void registerMavenSubmodules(
            Path pomFile,
            Map<String, ProjectModuleGraph.ProjectModule> modulesById,
            Map<ProjectModuleGraph.MavenCoordinates, ProjectModuleGraph.ProjectModule> modulesByCoordinates) throws IOException {
        String content = Files.readString(pomFile);
        Path parentDir = pomFile.getParent();
        Matcher moduleMatcher = MAVEN_MODULE.matcher(content);
        while (moduleMatcher.find()) {
            Path moduleDir = parentDir.resolve(moduleMatcher.group(1).trim()).normalize();
            Path childPom = moduleDir.resolve("pom.xml");
            if (Files.exists(childPom)) {
                registerMavenModule(childPom, modulesById, modulesByCoordinates);
            }
        }
    }

    private void registerMavenModule(
            Path pomFile,
            Map<String, ProjectModuleGraph.ProjectModule> modulesById,
            Map<ProjectModuleGraph.MavenCoordinates, ProjectModuleGraph.ProjectModule> modulesByCoordinates) throws IOException {
        String content = Files.readString(pomFile);
        Optional<ProjectModuleGraph.MavenCoordinates> coordinates = extractOwnMavenCoordinates(content);
        if (coordinates.isEmpty()) {
            return;
        }
        Path directory = pomFile.getParent().normalize();

        for (Map.Entry<String, ProjectModuleGraph.ProjectModule> entry : modulesById.entrySet()) {
            if (entry.getValue().directory().toAbsolutePath().normalize().equals(directory.toAbsolutePath().normalize())) {
                ProjectModuleGraph.ProjectModule merged = new ProjectModuleGraph.ProjectModule(
                        entry.getKey(),
                        directory,
                        ProjectModuleGraph.BuildSystem.MIXED,
                        coordinates.get());
                modulesById.put(entry.getKey(), merged);
                modulesByCoordinates.put(coordinates.get(), merged);
                return;
            }
        }

        String id = "maven:" + coordinates.get().key();
        ProjectModuleGraph.ProjectModule module = new ProjectModuleGraph.ProjectModule(
                id,
                directory,
                ProjectModuleGraph.BuildSystem.MAVEN,
                coordinates.get());
        modulesById.put(id, module);
        modulesByCoordinates.put(coordinates.get(), module);
    }

    static Optional<ProjectModuleGraph.MavenCoordinates> extractOwnMavenCoordinates(String pomContent) {
        String content = pomContent;
        int parentEnd = pomContent.indexOf("</parent>");
        if (parentEnd != -1) {
            content = pomContent.substring(parentEnd);
        }
        String groupId = extractFirstTag(content, "groupId").orElse(null);
        String artifactId = extractFirstTag(content, "artifactId").orElse(null);
        if (artifactId == null) {
            return Optional.empty();
        }
        if (groupId == null) {
            groupId = extractParentTag(pomContent, "groupId").orElse("unknown");
        }
        return Optional.of(new ProjectModuleGraph.MavenCoordinates(groupId, artifactId));
    }

    private static Optional<String> extractParentTag(String pomContent, String tag) {
        Matcher parentMatcher = Pattern.compile("<parent>.*?</parent>", Pattern.DOTALL).matcher(pomContent);
        if (!parentMatcher.find()) {
            return Optional.empty();
        }
        return extractFirstTag(parentMatcher.group(), tag);
    }

    private static Optional<String> extractFirstTag(String content, String tag) {
        Matcher matcher = Pattern.compile("<" + tag + ">(.*?)</" + tag + ">").matcher(content);
        if (matcher.find()) {
            return Optional.of(matcher.group(1).trim());
        }
        return Optional.empty();
    }

    private List<ProjectModuleGraph.ModuleDependency> parseGradleDependencies(
            ProjectModuleGraph.ProjectModule module,
            Map<String, ProjectModuleGraph.ProjectModule> modulesById,
            Map<String, Path> gradleModulePaths) throws IOException {
        List<ProjectModuleGraph.ModuleDependency> result = new ArrayList<>();
        Path buildFile = module.directory().resolve("build.gradle");
        if (!Files.exists(buildFile)) {
            buildFile = module.directory().resolve("build.gradle.kts");
        }
        if (!Files.exists(buildFile)) {
            return result;
        }
        String content = Files.readString(buildFile);
        for (String line : content.split("\\R")) {
            if (isTestGradleConfigurationLine(line)) {
                continue;
            }
            Matcher matcher = GRADLE_PROJECT_DEP.matcher(line);
            while (matcher.find()) {
                String gradlePath = normalizeGradlePath(matcher.group(1));
                String targetId = findGradleModuleId(gradlePath, modulesById, gradleModulePaths);
                if (targetId != null) {
                    result.add(new ProjectModuleGraph.ModuleDependency(
                            module.id(), targetId, null, ProjectModuleGraph.DependencyKind.LOCAL_PROJECT));
                }
            }
        }
        return result;
    }

    static boolean isTestScopedMavenDependency(String dependencyBlock) {
        return extractFirstTag(dependencyBlock, "scope")
                .map(scope -> scope.equalsIgnoreCase("test") || scope.equalsIgnoreCase("testCompile"))
                .orElse(false);
    }

    static boolean isTestGradleConfigurationLine(String line) {
        if (line == null || line.isBlank()) {
            return false;
        }
        String trimmed = line.trim();
        return trimmed.matches("(?i).*(testImplementation|testCompileOnly|testRuntimeOnly|testFixturesImplementation|androidTestImplementation|androidTestCompileOnly|androidTestRuntimeOnly|testAnnotationProcessor)\\b.*");
    }

    private static String findGradleModuleId(
            String gradlePath,
            Map<String, ProjectModuleGraph.ProjectModule> modulesById,
            Map<String, Path> gradleModulePaths) {
        String exact = "gradle:" + gradlePath;
        if (modulesById.containsKey(exact)) {
            return exact;
        }
        Path expectedDir = gradleModulePaths.get(gradlePath);
        if (expectedDir != null) {
            for (ProjectModuleGraph.ProjectModule module : modulesById.values()) {
                if (module.directory().toAbsolutePath().normalize().equals(expectedDir.toAbsolutePath().normalize())) {
                    return module.id();
                }
            }
        }
        return null;
    }

    private List<ProjectModuleGraph.ModuleDependency> parseMavenDependencies(
            Path pomFile,
            Map<String, ProjectModuleGraph.ProjectModule> modulesById,
            Map<ProjectModuleGraph.MavenCoordinates, ProjectModuleGraph.ProjectModule> modulesByCoordinates) throws IOException {
        List<ProjectModuleGraph.ModuleDependency> result = new ArrayList<>();
        String content = Files.readString(pomFile);
        Optional<ProjectModuleGraph.MavenCoordinates> fromCoordinates = extractOwnMavenCoordinates(content);
        if (fromCoordinates.isEmpty()) {
            return result;
        }
        ProjectModuleGraph.ProjectModule fromModule = modulesByCoordinates.get(fromCoordinates.get());
        if (fromModule == null) {
            return result;
        }

        int depsStart = content.indexOf("<dependencies>");
        int depsEnd = content.indexOf("</dependencies>");
        if (depsStart == -1 || depsEnd <= depsStart) {
            return result;
        }
        String depsSection = content.substring(depsStart, depsEnd);
        Matcher blockMatcher = MAVEN_DEPENDENCY_BLOCK.matcher(depsSection);
        while (blockMatcher.find()) {
            String block = blockMatcher.group(1);
            if (isTestScopedMavenDependency(block)) {
                continue;
            }
            String groupId = extractFirstTag(block, "groupId").orElse(null);
            String artifactId = extractFirstTag(block, "artifactId").orElse(null);
            if (artifactId == null) {
                continue;
            }
            if (groupId == null) {
                groupId = extractParentTag(content, "groupId").orElse("unknown");
            }
            ProjectModuleGraph.MavenCoordinates targetCoordinates = new ProjectModuleGraph.MavenCoordinates(groupId, artifactId);
            ProjectModuleGraph.ProjectModule targetModule = modulesByCoordinates.get(targetCoordinates);
            if (targetModule != null && !targetModule.id().equals(fromModule.id())) {
                result.add(new ProjectModuleGraph.ModuleDependency(
                        fromModule.id(),
                        targetModule.id(),
                        null,
                        ProjectModuleGraph.DependencyKind.LOCAL_PROJECT));
            } else {
                result.add(new ProjectModuleGraph.ModuleDependency(
                        fromModule.id(),
                        null,
                        targetCoordinates,
                        ProjectModuleGraph.DependencyKind.EXTERNAL_LIBRARY));
            }
        }
        return result;
    }

    private void ensureInputModule(Path inputDir, Map<String, ProjectModuleGraph.ProjectModule> modulesById) {
        Path normalized = inputDir.toAbsolutePath().normalize();
        boolean known = modulesById.values().stream()
                .anyMatch(module -> module.directory().toAbsolutePath().normalize().equals(normalized));
        if (!known) {
            modulesById.put("input:" + normalized.getFileName(), new ProjectModuleGraph.ProjectModule(
                    "input:" + normalized.getFileName(),
                    normalized,
                    ProjectModuleGraph.BuildSystem.MIXED,
                    null));
        }
    }

    private List<Path> findPomFiles(Path workspaceRoot) throws IOException {
        if (workspaceRoot == null || !Files.isDirectory(workspaceRoot)) {
            return List.of();
        }
        try (Stream<Path> stream = Files.walk(workspaceRoot)) {
            return stream.filter(path -> path.getFileName().toString().equals("pom.xml"))
                    .filter(path -> !path.toString().contains("/target/"))
                    .filter(path -> !path.toString().contains("\\target\\"))
                    .collect(Collectors.toList());
        }
    }

    Path findWorkspaceRoot(Path start) {
        if (start == null) {
            return null;
        }
        Path normalized = start.toAbsolutePath().normalize();
        Path current = normalized;
        Path deepestMavenAggregator = null;
        Path deepestGradleSettings = null;
        Path deepestPom = null;

        while (current != null) {
            if (Files.exists(current.resolve("settings.gradle")) || Files.exists(current.resolve("settings.gradle.kts"))) {
                deepestGradleSettings = current;
            }
            Path pom = current.resolve("pom.xml");
            if (Files.exists(pom)) {
                deepestPom = current;
                try {
                    if (Files.readString(pom).contains("<modules>")) {
                        deepestMavenAggregator = current;
                    }
                } catch (IOException ignored) {
                    // continue walking
                }
            }
            current = current.getParent();
        }

        if (deepestMavenAggregator != null) {
            return deepestMavenAggregator;
        }
        if (deepestGradleSettings != null) {
            return deepestGradleSettings;
        }
        if (deepestPom != null) {
            return deepestPom;
        }
        return normalized;
    }

    Path findIndexingRoot(Path start, Path workspaceRoot) {
        if (workspaceRoot == null) {
            return findIndexingRoot(start);
        }
        return workspaceRoot;
    }

    Path findIndexingRoot(Path start) {
        if (start == null) {
            return null;
        }
        Path current = start.toAbsolutePath().normalize();
        Path gitRoot = null;
        while (current != null) {
            if (Files.exists(current.resolve(".git"))) {
                gitRoot = current;
            }
            current = current.getParent();
        }
        return gitRoot != null ? gitRoot : findWorkspaceRoot(start);
    }
}
