package com.codechecker.api.analyzer;

import com.codechecker.analyzer.FieldDiffEngine;
import com.codechecker.api.model.EndpointSnapshot;
import com.codechecker.api.model.MethodParameterSnapshot;
import com.codechecker.api.model.ParameterChange;
import com.codechecker.api.parser.NestedDtoFieldParser;
import com.codechecker.api.parser.NestedFieldInfo;
import com.codechecker.model.FieldChange;
import com.codechecker.model.FieldInfo;

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

/**
 * 接口入参 diff（普通参数 + RequestBody 嵌套 Dto 字段）。
 *
 * path/query 规则：
 * - 形参名+类型相同，仅绑定名变 → 重命名
 * - 形参名+绑定名相同，仅类型变 → 类型变更
 * - 仅形参名变（绑定名不变）→ 不通知
 * - 类型与绑定名同时变，或三者都变 → 先删除后新增
 */
public class ParameterDiffEngine {
    private final NestedDtoFieldParser nestedDtoFieldParser;
    private final FieldDiffEngine fieldDiffEngine = new FieldDiffEngine();

    public ParameterDiffEngine(Path repoRoot, List<String> searchDirs) {
        this.nestedDtoFieldParser = new NestedDtoFieldParser(repoRoot, searchDirs);
    }

    public List<ParameterChange> diff(EndpointSnapshot oldSnap, EndpointSnapshot newSnap) throws IOException {
        List<ParameterChange> changes = new ArrayList<>();
        changes.addAll(diffBodyParams(oldSnap, newSnap));
        changes.addAll(diffBindingParams(oldSnap, newSnap));
        return changes;
    }

    private List<ParameterChange> diffBodyParams(EndpointSnapshot oldSnap, EndpointSnapshot newSnap)
            throws IOException {
        Map<String, MethodParameterSnapshot> oldParams = filterBySource(oldSnap, "body");
        Map<String, MethodParameterSnapshot> newParams = filterBySource(newSnap, "body");
        List<ParameterChange> changes = new ArrayList<>();

        for (Map.Entry<String, MethodParameterSnapshot> entry : newParams.entrySet()) {
            MethodParameterSnapshot oldParam = oldParams.get(entry.getKey());
            MethodParameterSnapshot newParam = entry.getValue();
            if (oldParam == null) {
                changes.addAll(addedBodyChanges(newParam));
            } else {
                changes.addAll(diffBodyDto(oldParam, newParam));
            }
        }
        for (Map.Entry<String, MethodParameterSnapshot> entry : oldParams.entrySet()) {
            if (!newParams.containsKey(entry.getKey())) {
                changes.addAll(removedBodyChanges(entry.getValue()));
            }
        }
        return changes;
    }

    private List<ParameterChange> diffBindingParams(EndpointSnapshot oldSnap, EndpointSnapshot newSnap) {
        Map<String, MethodParameterSnapshot> oldParams = filterBindingParams(oldSnap);
        Map<String, MethodParameterSnapshot> newParams = filterBindingParams(newSnap);
        List<ParameterChange> changes = new ArrayList<>();

        List<MethodParameterSnapshot> unmatchedOld = new ArrayList<>();
        List<MethodParameterSnapshot> unmatchedNew = new ArrayList<>();

        for (Map.Entry<String, MethodParameterSnapshot> entry : newParams.entrySet()) {
            MethodParameterSnapshot oldParam = oldParams.get(entry.getKey());
            MethodParameterSnapshot newParam = entry.getValue();
            if (oldParam == null) {
                unmatchedNew.add(newParam);
            } else if (!oldParam.getType().equals(newParam.getType())) {
                changes.add(ParameterChange.modified(
                        newParam.displayName(),
                        newParam.getType(),
                        newParam.getDescription(),
                        oldParam.getType() + " → " + newParam.getType(),
                        newParam.getSource(),
                        null, null, null));
            }
        }
        for (Map.Entry<String, MethodParameterSnapshot> entry : oldParams.entrySet()) {
            if (!newParams.containsKey(entry.getKey())) {
                unmatchedOld.add(entry.getValue());
            }
        }

        pairRenamedBindingParams(unmatchedOld, unmatchedNew, changes);

        for (MethodParameterSnapshot removed : unmatchedOld) {
            changes.add(ParameterChange.removed(
                    removed.displayName(), removed.getType(), removed.getDescription(),
                    removed.getSource(), null, null, null));
        }
        for (MethodParameterSnapshot added : unmatchedNew) {
            changes.add(ParameterChange.added(
                    added.displayName(), added.getType(), added.getDescription(),
                    added.getSource(), null, null, null));
        }
        return changes;
    }

    /** 形参名+类型相同，仅绑定名变化 → 重命名 */
    private void pairRenamedBindingParams(List<MethodParameterSnapshot> unmatchedOld,
                                          List<MethodParameterSnapshot> unmatchedNew,
                                          List<ParameterChange> changes) {
        Set<MethodParameterSnapshot> pairedOld = new HashSet<>();
        Set<MethodParameterSnapshot> pairedNew = new HashSet<>();

        for (MethodParameterSnapshot oldParam : unmatchedOld) {
            if (pairedOld.contains(oldParam)) {
                continue;
            }
            for (MethodParameterSnapshot newParam : unmatchedNew) {
                if (pairedNew.contains(newParam)) {
                    continue;
                }
                if (!oldParam.getSource().equals(newParam.getSource())) {
                    continue;
                }
                if (!oldParam.getName().equals(newParam.getName())) {
                    continue;
                }
                if (!oldParam.getType().equals(newParam.getType())) {
                    continue;
                }
                changes.add(ParameterChange.renamed(
                        oldParam.getBindingName(),
                        newParam.getBindingName(),
                        newParam.getType(),
                        newParam.getDescription(),
                        newParam.getSource(),
                        null, null, null));
                pairedOld.add(oldParam);
                pairedNew.add(newParam);
                break;
            }
        }
        unmatchedOld.removeIf(pairedOld::contains);
        unmatchedNew.removeIf(pairedNew::contains);
    }

    private Map<String, MethodParameterSnapshot> filterBindingParams(EndpointSnapshot snap) {
        Map<String, MethodParameterSnapshot> map = new LinkedHashMap<>();
        for (MethodParameterSnapshot p : snap.getParameters()) {
            if (isBindingParam(p)) {
                map.put(p.identityKey(), p);
            }
        }
        return map;
    }

    private Map<String, MethodParameterSnapshot> filterBySource(EndpointSnapshot snap, String source) {
        Map<String, MethodParameterSnapshot> map = new LinkedHashMap<>();
        for (MethodParameterSnapshot p : snap.getParameters()) {
            if (source.equals(p.getSource())) {
                map.put(p.identityKey(), p);
            }
        }
        return map;
    }

    private boolean isBindingParam(MethodParameterSnapshot param) {
        return "path".equals(param.getSource()) || "query".equals(param.getSource());
    }

    private List<ParameterChange> diffBodyDto(MethodParameterSnapshot oldParam,
                                                MethodParameterSnapshot newParam) throws IOException {
        List<NestedFieldInfo> oldFields = nestedDtoFieldParser.parseNestedFields(oldParam.getDtoClassName());
        List<NestedFieldInfo> newFields = nestedDtoFieldParser.parseNestedFields(newParam.getDtoClassName());
        List<FieldChange> fieldChanges = fieldDiffEngine.diff(toFieldInfo(oldFields), toFieldInfo(newFields));
        List<ParameterChange> result = new ArrayList<>();
        for (FieldChange fc : fieldChanges) {
            result.add(mapFieldChange(fc, newParam.getName(), newParam.getDtoClassName()));
        }
        return result;
    }

    private ParameterChange mapFieldChange(FieldChange fc, String bodyParamName, String dtoName) {
        String path = fc.getFieldName();
        switch (fc.getKind()) {
            case ADDED:
                return ParameterChange.added(path, fc.getNewType(), fc.getDescription(),
                        "body", bodyParamName, dtoName, path);
            case REMOVED:
                return ParameterChange.removed(path, fc.getOldType(), fc.getDescription(),
                        "body", bodyParamName, dtoName, path);
            case RENAMED:
                return ParameterChange.renamed(fc.getOldFieldName(), fc.getFieldName(),
                        fc.getNewType(), fc.getDescription(), "body", bodyParamName, dtoName, path);
            case MODIFIED:
            default:
                return ParameterChange.modified(path, fc.getNewType(), fc.getDescription(),
                        fc.getDetail(), "body", bodyParamName, dtoName, path);
        }
    }

    private List<ParameterChange> addedBodyChanges(MethodParameterSnapshot param) throws IOException {
        List<ParameterChange> list = new ArrayList<>();
        for (NestedFieldInfo field : nestedDtoFieldParser.parseNestedFields(param.getDtoClassName())) {
            list.add(ParameterChange.added(field.getPath(), field.getType(), field.getDescription(),
                    "body", param.getName(), param.getDtoClassName(), field.getPath()));
        }
        return list;
    }

    private List<ParameterChange> removedBodyChanges(MethodParameterSnapshot param) throws IOException {
        List<ParameterChange> list = new ArrayList<>();
        for (NestedFieldInfo field : nestedDtoFieldParser.parseNestedFields(param.getDtoClassName())) {
            list.add(ParameterChange.removed(field.getPath(), field.getType(), field.getDescription(),
                    "body", param.getName(), param.getDtoClassName(), field.getPath()));
        }
        return list;
    }

    private List<FieldInfo> toFieldInfo(List<NestedFieldInfo> nested) {
        List<FieldInfo> result = new ArrayList<>();
        for (NestedFieldInfo info : nested) {
            result.add(new FieldInfo(info.getPath(), info.getType(), info.getDescription()));
        }
        return result;
    }
}
