package com.spring.domain.endpoint.service;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import com.spring.common.util.TypeUtils;
import com.spring.common.util.TypeUtils.TypeInfo;
import com.spring.domain.endpoint.dto.EndpointInfo;

import lombok.RequiredArgsConstructor;

/**
 * EndpointCollectorService는 Spring MVC의 @RestController로 정의된 API 엔드포인트를 수집하는 서비스입니다.
 * 이 서비스는 RequestMappingHandlerMapping을 사용하여 모든 핸들러 메서드를 가져오고,
 * 각 메서드의 URI, HTTP 메서드, 클래스 이름, 메서드 이름, 파라미터 정보 및 반환 타입 정보를 포함하는 EndpointInfo 객체를 생성합니다.
 */
@Service
@RequiredArgsConstructor
public class EndpointCollectorService {
    
    private final RequestMappingHandlerMapping requestMappingHandlerMapping;

    /**
     * collectApiEndpoints 메서드는 모든 API 엔드포인트를 수집하여 EndpointInfo 객체의 리스트를 반환합니다.
     * @return 수집된 API 엔드포인트의 리스트
     */
    public List<EndpointInfo> collectApiEndpoints() {
        List<EndpointInfo> endpoints = new ArrayList<>();
        
        // 핸들러 메서드 맵 가져오기
        Map<RequestMappingInfo, HandlerMethod> handlerMethods = requestMappingHandlerMapping.getHandlerMethods();
        
        handlerMethods.forEach((info, method) -> {
            // @RestController가 붙은 클래스의 메서드만 처리
            if (method.getBeanType().isAnnotationPresent(RestController.class)) {
                
                Set<String> patterns = Optional.ofNullable(info.getPathPatternsCondition())
                    .map(condition -> condition.getPatternValues())
                    .orElse(Collections.emptySet());
                
                Set<String> methods = new HashSet<>();
                Optional.ofNullable(info.getMethodsCondition())
                    .ifPresent(methodsCondition -> 
                        methodsCondition.getMethods()
                            .forEach(requestMethod -> methods.add(requestMethod.name()))
                    );
                
                String className = method.getBeanType().getSimpleName();
                String methodName = method.getMethod().getName();
                List<TypeInfo> parameterInfos = extractParameterInfos(method.getMethod());
                TypeInfo returnTypeInfo = extractReturnTypeInfo(method.getMethod());
                
                patterns.forEach(pattern -> {
                    EndpointInfo endpoint = new EndpointInfo(
                        pattern,
                        methods.isEmpty() ? Collections.singleton("ALL") : methods,
                        className,
                        methodName,
                        parameterInfos,
                        returnTypeInfo
                    );
                    endpoints.add(endpoint);
                });
            }
        });
        
        return endpoints.stream()
                .sorted(Comparator.comparing(EndpointInfo::getUri))
                .collect(Collectors.toList());
    }

    /**
     * extractParameterInfos 메서드는 주어진 메서드의 파라미터 정보를 추출하여 리스트로 반환합니다.
     * @param method 파라미터 정보를 추출할 메서드
     * @return 메서드의 파라미터 정보 리스트
     */
    private List<TypeInfo> extractParameterInfos(Method method) {
        String requestMethod = TypeUtils.extractRequestMethod(method);
        return Arrays.stream(method.getParameters())
            .map(parameter -> TypeUtils.extractParameterInfo(parameter, requestMethod))
            .collect(Collectors.toList());
    }
    
    /**
     * extractReturnTypeInfo 메서드는 주어진 메서드의 반환 타입 정보를 추출하여 반환합니다.
     * @param method 반환 타입 정보를 추출할 메서드
     * @return 메서드의 반환 타입 정보
     */
    private TypeInfo extractReturnTypeInfo(Method method) {
        return TypeUtils.extractReturnTypeInfo(method.getGenericReturnType());
    }

}
