package com.aicheck.git;

import com.aicheck.model.ChangedClassFile;
import com.aicheck.model.ClassType;
import com.aicheck.parser.ClassDeclParser;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
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.List;
import java.util.Locale;
import java.util.Map;

/**
 * 执行 git diff，识别 Dto/Vo/Entity/Model 的修改、删除、重命名（含 R* 与同目录 D+A 配对）。
 */
public class GitChangeScanner {
    private final Path repoRoot;
    private final ClassDeclParser classDeclParser = new ClassDeclParser();

    public GitChangeScanner(Path repoRoot) {
        this.repoRoot = repoRoot;
    }

    /** 扫描两次提交间的模型类变更 */
    public List<ChangedClassFile> scanChangedClasses(String oldSha, String newSha) throws IOException {
        List<String> lines = runGit("diff", "--name-status", oldSha, newSha);
        List<ChangedClassFile> deletions = new ArrayList<>();
        Map<String, PendingAdd> additionsByParent = new LinkedHashMap<>();
        List<ChangedClassFile> result = new ArrayList<>();

        for (String line : lines) {
            if (line.isBlank()) {
                continue;
            }
            String[] parts = line.split("\t");
            if (parts.length < 2) {
                continue;
            }
            String status = parts[0].trim();

            if (status.startsWith("R") && parts.length >= 3) {
                ChangedClassFile renamed = buildRenamed(parts[1], parts[2], oldSha, newSha);
                if (renamed != null) {
                    result.add(renamed);
                }
                continue;
            }

            String path = normalizePath(parts[parts.length - 1]);
            if (!path.endsWith(".java")) {
                continue;
            }

            String fallbackName = ClassDeclParser.classNameFromPath(path);
            ClassType classType = ClassType.fromClassName(fallbackName);
            if (classType == null) {
                continue;
            }

            if (status.equals("A")) {
                String newSource = readFileAtCommit(newSha, path);
                String className = classDeclParser.resolveClassName(newSource, fallbackName);
                additionsByParent.computeIfAbsent(parentDir(path), k -> new PendingAdd())
                        .add(path, className, classType, newSource);
                continue;
            }

            if (status.equals("D")) {
                String oldSource = readFileAtCommit(oldSha, path);
                String className = classDeclParser.resolveClassName(oldSource, fallbackName);
                deletions.add(new ChangedClassFile(path, ChangedClassFile.ChangeStatus.DELETED, className, classType));
                continue;
            }

            if (status.startsWith("M")) {
                ChangedClassFile modified = buildModified(path, oldSha, newSha, fallbackName, classType);
                if (modified != null) {
                    result.add(modified);
                }
            }
        }

        pairDeleteAndAdd(deletions, additionsByParent, oldSha, result);
        result.addAll(deletions);
        return result;
    }

    /** 处理 git R 状态：路径重命名 */
    private ChangedClassFile buildRenamed(String oldPathRaw, String newPathRaw,
                                          String oldSha, String newSha) throws IOException {
        String oldPath = normalizePath(oldPathRaw);
        String newPath = normalizePath(newPathRaw);
        if (!oldPath.endsWith(".java") || !newPath.endsWith(".java")) {
            return null;
        }

        String oldFallback = ClassDeclParser.classNameFromPath(oldPath);
        String newFallback = ClassDeclParser.classNameFromPath(newPath);
        ClassType classType = ClassType.fromClassName(newFallback);
        if (classType == null) {
            classType = ClassType.fromClassName(oldFallback);
        }
        if (classType == null) {
            return null;
        }

        String oldSource = readFileAtCommit(oldSha, oldPath);
        String newSource = readFileAtCommit(newSha, newPath);
        String oldClassName = classDeclParser.resolveClassName(oldSource, oldFallback);
        String newClassName = classDeclParser.resolveClassName(newSource, newFallback);

        return new ChangedClassFile(newPath, oldPath, ChangedClassFile.ChangeStatus.RENAMED,
                newClassName, oldClassName, classType);
    }

    /** 处理 M 状态：同路径下对比 AST 类名判断是否重命名 */
    private ChangedClassFile buildModified(String path, String oldSha, String newSha,
                                           String fallbackName, ClassType classType) throws IOException {
        String oldSource = readFileAtCommit(oldSha, path);
        String newSource = readFileAtCommit(newSha, path);
        if (newSource == null || newSource.isBlank()) {
            newSource = readFileAtHead(path);
        }
        String oldClassName = classDeclParser.resolveClassName(oldSource, fallbackName);
        String newClassName = classDeclParser.resolveClassName(newSource, fallbackName);

        if (oldClassName.equals(newClassName)) {
            return new ChangedClassFile(path, ChangedClassFile.ChangeStatus.MODIFIED,
                    newClassName, classType);
        }
        return new ChangedClassFile(path, path, ChangedClassFile.ChangeStatus.RENAMED,
                newClassName, oldClassName, classType);
    }

    /** 同目录 D+A 配对为 RENAMED（Git 未显式标记 R 时） */
    private void pairDeleteAndAdd(List<ChangedClassFile> deletions,
                                  Map<String, PendingAdd> additionsByParent,
                                  String oldSha,
                                  List<ChangedClassFile> result) throws IOException {
        List<ChangedClassFile> unpaired = new ArrayList<>();

        for (ChangedClassFile deleted : deletions) {
            String parent = parentDir(deleted.getRelativePath());
            PendingAdd pending = additionsByParent.get(parent);
            if (pending == null || pending.isEmpty()) {
                unpaired.add(deleted);
                continue;
            }

            PendingAdd.Candidate candidate = pending.poll(deleted.getClassType());
            if (candidate == null) {
                unpaired.add(deleted);
                continue;
            }

            String oldSource = readFileAtCommit(oldSha, deleted.getRelativePath());
            String oldClassName = classDeclParser.resolveClassName(oldSource, deleted.getClassName());
            result.add(new ChangedClassFile(candidate.path(), deleted.getRelativePath(),
                    ChangedClassFile.ChangeStatus.RENAMED,
                    candidate.className(), oldClassName, deleted.getClassType()));
        }

        deletions.clear();
        deletions.addAll(unpaired);
    }

    /** 取路径父目录，用于 D+A 配对 */
    private static String parentDir(String path) {
        int idx = path.lastIndexOf('/');
        return idx >= 0 ? path.substring(0, idx) : "";
    }

    /** 读取指定 commit 下的文件内容 */
    public String readFileAtCommit(String commitSha, String relativePath) throws IOException {
        List<String> lines = runGit("show", commitSha + ":" + relativePath);
        if (lines.isEmpty()) {
            return "";
        }
        if (lines.size() == 1 && lines.get(0).startsWith("fatal:")) {
            return "";
        }
        return String.join("\n", lines);
    }

    /** 读取工作区 HEAD 文件（commit 中缺失时的回退） */
    public String readFileAtHead(String relativePath) throws IOException {
        Path file = repoRoot.resolve(relativePath);
        if (!Files.exists(file)) {
            return null;
        }
        return Files.readString(file, StandardCharsets.UTF_8);
    }

    /** 在 repoRoot 下执行 git 命令并返回 stdout 行 */
    private List<String> runGit(String... args) throws IOException {
        String[] command = new String[args.length + 3];
        command[0] = "git";
        command[1] = "-C";
        command[2] = repoRoot.toString();
        System.arraycopy(args, 0, command, 3, args.length);

        ProcessBuilder builder = new ProcessBuilder(command);
        builder.redirectErrorStream(true);
        Process process = builder.start();

        List<String> output = new ArrayList<>();
        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
            String line;
            while ((line = reader.readLine()) != null) {
                output.add(line);
            }
        }

        try {
            int exitCode = process.waitFor();
            if (exitCode != 0 && !isBenignGitShowFailure(args, output)) {
                throw new IOException("git 命令失败: " + String.join(" ", command)
                        + "\n" + String.join("\n", output));
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new IOException("git 命令被中断", e);
        }
        return output;
    }

    /** git show 文件不存在等情况视为可忽略 */
    private boolean isBenignGitShowFailure(String[] args, List<String> output) {
        if (args.length > 0 && "show".equals(args[0])) {
            String joined = String.join("\n", output).toLowerCase(Locale.ROOT);
            return joined.contains("exists on disk") || joined.contains("bad object")
                    || joined.contains("path") && joined.contains("does not exist");
        }
        return false;
    }

    /** 统一路径分隔符为 / */
    private String normalizePath(String path) {
        return path.replace("\\", "/");
    }

    /** 同目录新增文件缓冲，供 D+A 配对 */
    private static final class PendingAdd {
        private final Map<ClassType, List<Candidate>> byType = new HashMap<>();

        void add(String path, String className, ClassType classType, String source) {
            byType.computeIfAbsent(classType, k -> new ArrayList<>())
                    .add(new Candidate(path, className));
        }

        boolean isEmpty() {
            return byType.values().stream().allMatch(List::isEmpty);
        }

        /** 按类型取出一个候选新增文件 */
        Candidate poll(ClassType classType) {
            List<Candidate> list = byType.get(classType);
            if (list == null || list.isEmpty()) {
                return null;
            }
            return list.remove(0);
        }

        private static final class Candidate {
            private final String path;
            private final String className;

            private Candidate(String path, String className) {
                this.path = path;
                this.className = className;
            }

            private String path() {
                return path;
            }

            private String className() {
                return className;
            }
        }
    }
}
