package com.codechecker.api.parser;

import com.codechecker.api.model.EndpointSnapshot;
import com.codechecker.api.model.MethodParameterSnapshot;
import com.codechecker.parser.TypeNameUtils;
import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.NodeList;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.body.Parameter;
import com.github.javaparser.ast.body.TypeDeclaration;
import com.github.javaparser.ast.expr.AnnotationExpr;
import com.github.javaparser.ast.expr.Expression;
import com.github.javaparser.ast.expr.NormalAnnotationExpr;
import com.github.javaparser.ast.type.Type;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

/**
 * 解析 Controller / Feign 接口完整快照（含入参明细）。
 */
public class EndpointSnapshotParser {
    private static final Set<String> MAPPING_ANNOTATIONS = Set.of(
            "GetMapping", "PostMapping", "PutMapping", "DeleteMapping", "PatchMapping", "RequestMapping"
    );
    private static final Map<String, String> MAPPING_DEFAULT_METHOD = Map.of(
            "GetMapping", "GET",
            "PostMapping", "POST",
            "PutMapping", "PUT",
            "DeleteMapping", "DELETE",
            "PatchMapping", "PATCH"
    );
    private static final Set<String> FRAMEWORK_PARAM_TYPES = Set.of(
            "HttpServletRequest", "HttpServletResponse", "BindingResult", "Principal",
            "Authentication", "Model", "ModelMap", "UriComponentsBuilder", "WebRequest",
            "NativeWebRequest", "Errors", "Locale"
    );

    private final boolean excludeFrameworkParams;

    public EndpointSnapshotParser(boolean excludeFrameworkParams) {
        this.excludeFrameworkParams = excludeFrameworkParams;
    }

    public List<EndpointSnapshot> parseSource(String source, String sourceFile, boolean feignMode) {
        if (source == null || source.isBlank()) {
            return List.of();
        }
        CompilationUnit cu = StaticJavaParser.parse(source);
        List<EndpointSnapshot> snapshots = new ArrayList<>();
        for (TypeDeclaration<?> type : cu.getTypes()) {
            if (!(type instanceof ClassOrInterfaceDeclaration)) {
                continue;
            }
            ClassOrInterfaceDeclaration decl = (ClassOrInterfaceDeclaration) type;
            if (feignMode && !isFeignClient(decl)) {
                continue;
            }
            if (!feignMode && !isController(decl)) {
                continue;
            }
            String basePath = feignMode
                    ? joinPaths(extractFeignBasePath(decl), extractTypeLevelPath(decl))
                    : extractTypeLevelPath(decl);
            String className = decl.getNameAsString();
            for (MethodDeclaration method : decl.getMethods()) {
                if (feignMode && !decl.isInterface()) {
                    continue;
                }
                if (!feignMode && decl.isInterface()) {
                    continue;
                }
                snapshots.addAll(parseMethod(method, basePath, sourceFile, className));
            }
        }
        return snapshots;
    }

    private List<EndpointSnapshot> parseMethod(MethodDeclaration method, String basePath,
                                              String sourceFile, String className) {
        List<EndpointSnapshot> result = new ArrayList<>();
        for (AnnotationExpr annotation : method.getAnnotations()) {
            String annName = annotation.getNameAsString();
            if (!MAPPING_ANNOTATIONS.contains(annName)) {
                continue;
            }
            List<String> subPaths = readStringArray(annotation, "value", "path");
            List<String> httpMethods = extractHttpMethods(annotation, annName);
            List<MethodParameterSnapshot> params = extractParameters(method);
            String methodDescription = MethodDescriptionExtractor.extract(method);
            String fingerprint = EndpointSnapshot.buildFingerprint(sourceFile, method.getNameAsString());
            for (String httpMethod : httpMethods) {
                for (String subPath : subPaths) {
                    String uri = joinPaths(basePath, subPath);
                    result.add(new EndpointSnapshot(fingerprint, httpMethod, uri, sourceFile,
                            className, method.getNameAsString(), methodDescription, params));
                }
            }
        }
        return result;
    }

    private List<MethodParameterSnapshot> extractParameters(MethodDeclaration method) {
        Map<String, String> paramDescriptions = MethodParamJavadocExtractor.extract(method);
        List<MethodParameterSnapshot> params = new ArrayList<>();
        for (Parameter parameter : method.getParameters()) {
            String typeName = TypeNameUtils.typeToString(parameter.getType());
            String simple = TypeNameUtils.simpleName(typeName);
            if (excludeFrameworkParams && FRAMEWORK_PARAM_TYPES.contains(simple)) {
                continue;
            }
            String source = resolveParamSource(parameter);
            String paramName = parameter.getNameAsString();
            String bindingName = resolveBindingName(parameter, source, paramName);
            boolean required = resolveRequired(parameter, source);
            String dtoName = "body".equals(source) ? simple : "";
            String description = paramDescriptions.getOrDefault(paramName, "");
            params.add(new MethodParameterSnapshot(
                    paramName,
                    bindingName,
                    typeName,
                    source,
                    required,
                    description,
                    dtoName
            ));
        }
        return params;
    }

    private String resolveBindingName(Parameter parameter, String source, String paramName) {
        if (!"path".equals(source) && !"query".equals(source)) {
            return paramName;
        }
        String annName = "path".equals(source) ? "PathVariable" : "RequestParam";
        for (AnnotationExpr ann : parameter.getAnnotations()) {
            if (!annName.equals(ann.getNameAsString())) {
                continue;
            }
            List<String> bindings = readStringArray(ann, "value", "name");
            for (String binding : bindings) {
                if (binding != null && !binding.isBlank()) {
                    return binding;
                }
            }
        }
        return paramName;
    }

    private String resolveParamSource(Parameter parameter) {
        for (AnnotationExpr ann : parameter.getAnnotations()) {
            String name = ann.getNameAsString();
            if ("RequestBody".equals(name)) {
                return "body";
            }
            if ("PathVariable".equals(name)) {
                return "path";
            }
            if ("RequestParam".equals(name)) {
                return "query";
            }
        }
        return "simple";
    }

    private boolean resolveRequired(Parameter parameter, String source) {
        if ("query".equals(source)) {
            for (AnnotationExpr ann : parameter.getAnnotations()) {
                if ("RequestParam".equals(ann.getNameAsString()) && ann.isNormalAnnotationExpr()) {
                    for (var pair : ann.asNormalAnnotationExpr().getPairs()) {
                        if ("required".equals(pair.getNameAsString())) {
                            return !"false".equalsIgnoreCase(pair.getValue().toString().trim());
                        }
                    }
                }
            }
        }
        return !"query".equals(source);
    }

    private boolean isController(ClassOrInterfaceDeclaration decl) {
        return decl.getAnnotations().stream()
                .anyMatch(ann -> {
                    String n = ann.getNameAsString();
                    return "RestController".equals(n) || "Controller".equals(n);
                });
    }

    private boolean isFeignClient(ClassOrInterfaceDeclaration decl) {
        return decl.isInterface() && decl.getAnnotations().stream()
                .anyMatch(ann -> "FeignClient".equals(ann.getNameAsString()));
    }

    private String extractTypeLevelPath(ClassOrInterfaceDeclaration decl) {
        for (AnnotationExpr annotation : decl.getAnnotations()) {
            if ("RequestMapping".equals(annotation.getNameAsString())) {
                List<String> paths = readStringArray(annotation, "value", "path");
                if (!paths.isEmpty()) {
                    return paths.get(0);
                }
            }
        }
        return "";
    }

    private String extractFeignBasePath(ClassOrInterfaceDeclaration decl) {
        for (AnnotationExpr annotation : decl.getAnnotations()) {
            if ("FeignClient".equals(annotation.getNameAsString())) {
                List<String> paths = readStringArray(annotation, "path");
                if (!paths.isEmpty()) {
                    return paths.get(0);
                }
            }
        }
        return "";
    }

    private List<String> extractHttpMethods(AnnotationExpr annotation, String annName) {
        if (!"RequestMapping".equals(annName)) {
            return List.of(MAPPING_DEFAULT_METHOD.getOrDefault(annName, "GET"));
        }
        List<String> methods = readEnumArray(annotation, "method");
        return methods.isEmpty() ? List.of("GET") : methods;
    }

    private String joinPaths(String base, String sub) {
        String normalizedBase = normalizePath(base);
        String normalizedSub = normalizePath(sub);
        if (normalizedBase.isEmpty()) {
            return normalizedSub.isEmpty() ? "/" : normalizedSub;
        }
        if (normalizedSub.isEmpty()) {
            return normalizedBase;
        }
        return (normalizedBase + "/" + normalizedSub.substring(1)).replaceAll("/+", "/");
    }

    private String normalizePath(String path) {
        if (path == null || path.isBlank()) {
            return "";
        }
        String trimmed = path.trim();
        if (!trimmed.startsWith("/")) {
            trimmed = "/" + trimmed;
        }
        return trimmed.replaceAll("/+", "/");
    }

    private List<String> readStringArray(AnnotationExpr annotation, String... keys) {
        NodeList<?> values = readArrayValues(annotation, keys);
        List<String> result = new ArrayList<>();
        for (Object value : values) {
            String text = value.toString().replace("\"", "").trim();
            if (!text.isBlank()) {
                result.add(text);
            }
        }
        if (result.isEmpty()) {
            result.add("");
        }
        return result;
    }

    private List<String> readEnumArray(AnnotationExpr annotation, String key) {
        NodeList<?> values = readArrayValues(annotation, key);
        List<String> result = new ArrayList<>();
        for (Object value : values) {
            String text = value.toString().trim();
            if (text.contains(".")) {
                text = text.substring(text.lastIndexOf('.') + 1);
            }
            result.add(text.toUpperCase(Locale.ROOT));
        }
        return result;
    }

    private NodeList<?> readArrayValues(AnnotationExpr annotation, String... keys) {
        if (annotation.isSingleMemberAnnotationExpr()) {
            Expression value = annotation.asSingleMemberAnnotationExpr().getMemberValue();
            if (value.isArrayInitializerExpr()) {
                return value.asArrayInitializerExpr().getValues();
            }
            return new NodeList<>(value);
        }
        if (annotation.isNormalAnnotationExpr()) {
            var pairs = annotation.asNormalAnnotationExpr().getPairs();
            for (var pair : pairs) {
                for (String key : keys) {
                    if (pair.getNameAsString().equals(key)) {
                        if (pair.getValue().isArrayInitializerExpr()) {
                            return pair.getValue().asArrayInitializerExpr().getValues();
                        }
                        return new NodeList<>(pair.getValue());
                    }
                }
            }
            for (var pair : pairs) {
                if ("value".equals(pair.getNameAsString())) {
                    if (pair.getValue().isArrayInitializerExpr()) {
                        return pair.getValue().asArrayInitializerExpr().getValues();
                    }
                    return new NodeList<>(pair.getValue());
                }
            }
        }
        return new NodeList<>();
    }
}
