package com.codechecker.notify;

import com.codechecker.analyzer.DtoNestIndex;
import com.codechecker.api.model.ApiChangeKind;
import com.codechecker.api.model.EndpointChangeReport;
import com.codechecker.api.model.ParameterChange;
import com.codechecker.config.DtoOverlapMode;
import com.codechecker.model.ApiEndpoint;
import com.codechecker.model.ClassChangeReport;
import com.codechecker.model.ClassType;

import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

/**
 * 按配置过滤 Dto 类变更与 API 参数变更的重叠通知。
 */
public class OverlapNotificationFilter {

    public static final class FilterResult {
        private final List<ClassChangeReport> classReports;
        private final List<EndpointChangeReport> apiReports;

        public FilterResult(List<ClassChangeReport> classReports, List<EndpointChangeReport> apiReports) {
            this.classReports = classReports;
            this.apiReports = apiReports;
        }

        public List<ClassChangeReport> classReports() {
            return classReports;
        }

        public List<EndpointChangeReport> apiReports() {
            return apiReports;
        }
    }

    public static FilterResult apply(List<ClassChangeReport> classReports,
                                     List<EndpointChangeReport> apiReports,
                                     DtoOverlapMode mode,
                                     DtoNestIndex nestIndex) {
        if (mode == DtoOverlapMode.BOTH) {
            return new FilterResult(classReports, apiReports);
        }
        Set<OverlapKey> overlapKeys = buildOverlapKeys(classReports, nestIndex);
        if (overlapKeys.isEmpty()) {
            return new FilterResult(classReports, apiReports);
        }
        if (mode == DtoOverlapMode.CLASS_ONLY) {
            return new FilterResult(classReports, filterApiReports(apiReports, overlapKeys));
        }
        Set<OverlapKey> apiOverlapKeys = buildApiOverlapKeys(apiReports);
        return new FilterResult(filterClassReportsForApiOnly(classReports, apiOverlapKeys, nestIndex), apiReports);
    }

    /**
     * 重叠键使用 @RequestBody 根 Dto（如 PunishmentsApprovalDto），与 API 参数通知 parentDto 对齐；
     * 嵌套子 Dto（如 UserSelfDto）通过 nestIndex 解析到根 Dto。
     */
    private static Set<OverlapKey> buildOverlapKeys(List<ClassChangeReport> classReports,
                                                    DtoNestIndex nestIndex) {
        Set<OverlapKey> keys = new LinkedHashSet<>();
        for (ClassChangeReport report : classReports) {
            if (report.getClassType() != ClassType.DTO) {
                continue;
            }
            if (!hasDtoFieldChanges(report)) {
                continue;
            }
            Set<String> bodyRoots = requestBodyRoots(report, nestIndex);
            for (ApiEndpoint endpoint : report.getInputImpactEndpoints()) {
                for (String rootDto : bodyRoots) {
                    keys.add(new OverlapKey(rootDto, endpoint.endpointKey()));
                }
            }
        }
        return keys;
    }

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

    private static List<EndpointChangeReport> filterApiReports(List<EndpointChangeReport> apiReports,
                                                               Set<OverlapKey> overlapKeys) {
        List<EndpointChangeReport> kept = new ArrayList<>();
        for (EndpointChangeReport report : apiReports) {
            if (!matchesOverlap(report, overlapKeys)) {
                kept.add(report);
            }
        }
        return kept;
    }

    private static List<ClassChangeReport> filterClassReportsForApiOnly(List<ClassChangeReport> classReports,
                                                                        Set<OverlapKey> apiOverlapKeys,
                                                                        DtoNestIndex nestIndex) {
        List<ClassChangeReport> kept = new ArrayList<>();
        for (ClassChangeReport report : classReports) {
            if (!shouldSuppressClassForApiOnly(report, apiOverlapKeys, nestIndex)) {
                kept.add(report);
            }
        }
        return kept;
    }

    private static Set<OverlapKey> buildApiOverlapKeys(List<EndpointChangeReport> apiReports) {
        Set<OverlapKey> keys = new LinkedHashSet<>();
        for (EndpointChangeReport report : apiReports) {
            if (report.getChangeKind() != ApiChangeKind.PARAM_CHANGED) {
                continue;
            }
            String endpointKey = report.getHttpMethod() + " " + report.getUri();
            for (ParameterChange change : report.getParameterChanges()) {
                if (!"body".equals(change.getSource())) {
                    continue;
                }
                String parentDto = change.getParentDto();
                if (parentDto != null && !parentDto.isBlank()) {
                    keys.add(new OverlapKey(parentDto, endpointKey));
                }
            }
            if (report.isDtoFollowUp()) {
                String relatedDto = report.getRelatedDtoClassName();
                if (relatedDto != null && !relatedDto.isBlank()) {
                    keys.add(new OverlapKey(relatedDto, endpointKey));
                }
            }
        }
        return keys;
    }

    private static boolean shouldSuppressClassForApiOnly(ClassChangeReport report,
                                                         Set<OverlapKey> apiOverlapKeys,
                                                         DtoNestIndex nestIndex) {
        if (report.getClassType() != ClassType.DTO || !hasDtoFieldChanges(report)) {
            return false;
        }
        Set<String> bodyRoots = requestBodyRoots(report, nestIndex);
        for (ApiEndpoint endpoint : report.getInputImpactEndpoints()) {
            for (String rootDto : bodyRoots) {
                if (apiOverlapKeys.contains(new OverlapKey(rootDto, endpoint.endpointKey()))) {
                    return true;
                }
            }
        }
        return false;
    }

    private static boolean matchesOverlap(EndpointChangeReport report, Set<OverlapKey> overlapKeys) {
        if (report.getChangeKind() != ApiChangeKind.PARAM_CHANGED) {
            return false;
        }
        String endpointKey = report.getHttpMethod() + " " + report.getUri();
        for (ParameterChange change : report.getParameterChanges()) {
            if (!"body".equals(change.getSource())) {
                continue;
            }
            String parentDto = change.getParentDto();
            if (parentDto == null || parentDto.isBlank()) {
                continue;
            }
            if (overlapKeys.contains(new OverlapKey(parentDto, endpointKey))) {
                return true;
            }
        }
        if (report.isDtoFollowUp()) {
            String relatedDto = report.getRelatedDtoClassName();
            if (relatedDto != null && overlapKeys.contains(new OverlapKey(relatedDto, endpointKey))) {
                return true;
            }
        }
        return false;
    }

    private static boolean hasDtoFieldChanges(ClassChangeReport report) {
        return !report.getFieldChanges().isEmpty();
    }

    private static final class OverlapKey {
        private final String dtoClassName;
        private final String endpointKey;

        private OverlapKey(String dtoClassName, String endpointKey) {
            this.dtoClassName = dtoClassName;
            this.endpointKey = endpointKey;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (!(obj instanceof OverlapKey)) {
                return false;
            }
            OverlapKey other = (OverlapKey) obj;
            return dtoClassName.equals(other.dtoClassName) && endpointKey.equals(other.endpointKey);
        }

        @Override
        public int hashCode() {
            return dtoClassName.hashCode() * 31 + endpointKey.hashCode();
        }
    }
}
