package com.codechecker.api.analyzer;

import com.codechecker.analyzer.DtoNestIndex;
import com.codechecker.api.model.ApiChangeKind;
import com.codechecker.api.model.EndpointChangeReport;
import com.codechecker.api.model.EndpointSnapshot;
import com.codechecker.api.model.ParameterChange;
import com.codechecker.api.parser.EndpointSnapshotParser;
import com.codechecker.config.AppConfig;
import com.codechecker.git.GitChangeScanner;
import com.codechecker.model.ApiEndpoint;
import com.codechecker.model.ClassChangeReport;
import com.codechecker.model.ClassType;

import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * 类变更（Dto/Vo 嵌套字段）后，对受影响的 Controller 继续 API 参数 diff，产出 PARAM_CHANGED 报告。
 */
public class DtoImpactedApiAnalyzer {
    private final GitChangeScanner gitScanner;

    public DtoImpactedApiAnalyzer(GitChangeScanner gitScanner) {
        this.gitScanner = gitScanner;
    }

    public List<EndpointChangeReport> analyze(Path repoRoot, AppConfig config,
                                              String oldSha, String newSha,
                                              List<ClassChangeReport> classReports,
                                              Set<String> alreadyScannedFiles,
                                              DtoNestIndex nestIndex) throws IOException {
        Map<String, Set<String>> controllerToDtos = collectImpactedControllers(classReports, alreadyScannedFiles,
                nestIndex);
        if (controllerToDtos.isEmpty()) {
            return List.of();
        }

        EndpointSnapshotParser parser = new EndpointSnapshotParser(config.isApiExcludeFrameworkParams());
        ParameterDiffEngine parameterDiffEngine = new ParameterDiffEngine(
                repoRoot, buildSearchDirs(config), gitScanner, oldSha, newSha, config.getNestMaxDepth());
        EndpointDiffEngine endpointDiffEngine = new EndpointDiffEngine(parameterDiffEngine);

        List<EndpointSnapshot> oldSnapshots = new ArrayList<>();
        List<EndpointSnapshot> newSnapshots = new ArrayList<>();
        for (String path : controllerToDtos.keySet()) {
            boolean feign = isFeignPath(path, config);
            String oldSource = gitScanner.readFileAtCommit(oldSha, path);
            String newSource = gitScanner.readFileAtCommit(newSha, path);
            oldSnapshots.addAll(parser.parseSource(oldSource, path, feign));
            newSnapshots.addAll(parser.parseSource(newSource, path, feign));
        }

        List<EndpointChangeReport> reports = new ArrayList<>();
        for (EndpointChangeReport report : endpointDiffEngine.diff(oldSnapshots, newSnapshots)) {
            if (report.getChangeKind() != ApiChangeKind.PARAM_CHANGED || !report.hasParameterChanges()) {
                continue;
            }
            String relatedDto = findRelatedDto(report, controllerToDtos);
            if (relatedDto == null) {
                continue;
            }
            reports.add(EndpointChangeReport.dtoFollowUp(report, relatedDto));
        }
        return reports;
    }

    private Map<String, Set<String>> collectImpactedControllers(List<ClassChangeReport> classReports,
                                                                Set<String> alreadyScannedFiles,
                                                                DtoNestIndex nestIndex) {
        Map<String, Set<String>> controllerToDtos = new LinkedHashMap<>();
        for (ClassChangeReport report : classReports) {
            if (report.getFieldChanges().isEmpty()) {
                continue;
            }
            if (report.getClassType() != ClassType.DTO && report.getClassType() != ClassType.VO) {
                continue;
            }
            Set<String> bodyRoots = resolveBodyRoots(report, nestIndex);
            if (bodyRoots.isEmpty()) {
                continue;
            }
            for (ApiEndpoint endpoint : report.getInputImpactEndpoints()) {
                String controllerFile = endpoint.getSourceFile();
                if (alreadyScannedFiles.contains(controllerFile)) {
                    continue;
                }
                controllerToDtos.computeIfAbsent(controllerFile, k -> new LinkedHashSet<>()).addAll(bodyRoots);
            }
        }
        return controllerToDtos;
    }

    private Set<String> resolveBodyRoots(ClassChangeReport report, DtoNestIndex nestIndex) {
        if (nestIndex == null) {
            Set<String> names = new LinkedHashSet<>();
            if (report.getClassName().endsWith("Dto")) {
                names.add(report.getClassName());
            }
            return names;
        }
        Set<String> roots = new LinkedHashSet<>();
        roots.addAll(nestIndex.findRequestBodyRoots(report.getClassName()));
        if (report.getOldClassName() != null && !report.getOldClassName().isBlank()) {
            roots.addAll(nestIndex.findRequestBodyRoots(report.getOldClassName()));
        }
        return roots;
    }

    private String findRelatedDto(EndpointChangeReport report, Map<String, Set<String>> controllerToDtos) {
        Set<String> impactedDtos = controllerToDtos.getOrDefault(report.getSourceFile(), Set.of());
        for (ParameterChange change : report.getParameterChanges()) {
            if (!"body".equals(change.getSource())) {
                continue;
            }
            String parentDto = change.getParentDto();
            if (parentDto != null && impactedDtos.contains(parentDto)) {
                return parentDto;
            }
        }
        return null;
    }

    private List<String> buildSearchDirs(AppConfig config) {
        List<String> dirs = new ArrayList<>();
        dirs.addAll(config.getModelDirs());
        dirs.addAll(config.getAllApiScanDirs());
        return dirs;
    }

    private boolean isFeignPath(String path, AppConfig config) {
        String normalized = path.replace('\\', '/');
        for (String dir : config.getApiFeignScanDirs()) {
            String prefix = dir.replace('\\', '/');
            if (!prefix.endsWith("/")) {
                prefix = prefix + "/";
            }
            if (normalized.startsWith(prefix)) {
                return true;
            }
        }
        return false;
    }
}
