package com.cmvr.web.core.config;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import io.swagger.v3.core.converter.AnnotatedType;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
import org.springdoc.core.models.GroupedOpenApi;
import org.springdoc.core.customizers.OpenApiCustomizer;
import org.springdoc.core.customizers.OperationCustomizer;
import org.springdoc.core.customizers.PropertyCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.MethodParameter;
import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;

/** OpenAPI metadata, authorization and business-domain groups. */
@Configuration
public class OpenApiConfig
{
    private static final String AUTHORIZATION = "Authorization";

    @Bean
    public OpenAPI cmvrOpenApi()
    {
        SecurityScheme bearerAuth = new SecurityScheme()
                .type(SecurityScheme.Type.HTTP)
                .scheme("bearer")
                .bearerFormat("JWT");
        return new OpenAPI()
                .info(new Info()
                        .title("CMVR IoT API")
                        .description("招商车研物联网平台接口文档")
                        .version("1.0.0"))
                .components(new Components().addSecuritySchemes(AUTHORIZATION, bearerAuth))
                .addSecurityItem(new SecurityRequirement().addList(AUTHORIZATION));
    }

    @Bean
    public GroupedOpenApi allApi()
    {
        return group("00-all", "全部接口", "/**");
    }

    private GroupedOpenApi group(String id, String displayName, String... paths)
    {
        return GroupedOpenApi.builder()
                .group(id)
                .displayName(displayName)
                .pathsToMatch(paths)
                // Keep the same visibility rule as the legacy Swagger Docket.
                .addOpenApiMethodFilter(method -> method.isAnnotationPresent(ApiOperation.class))
                .addOperationCustomizer(legacySwaggerOperationCustomizer())
                .addOpenApiCustomizer(legacyTagOrderCustomizer())
                .build();
    }

    private OpenApiCustomizer legacyTagOrderCustomizer()
    {
        return openApi -> {
            Set<String> tagNames = new TreeSet<>();
            if (openApi.getPaths() != null)
            {
                openApi.getPaths().values().forEach(pathItem -> pathItem.readOperations().forEach(operation -> {
                    if (operation.getTags() != null)
                    {
                        tagNames.addAll(operation.getTags());
                    }
                }));
            }
            openApi.setTags(tagNames.stream()
                    .map(name -> new io.swagger.v3.oas.models.tags.Tag().name(name))
                    .collect(Collectors.toList()));
        };
    }

    @Bean
    public OperationCustomizer legacySwaggerOperationCustomizer()
    {
        return this::customizeLegacyOperation;
    }

    @Bean
    public PropertyCustomizer legacySwaggerPropertyCustomizer()
    {
        return (schema, annotatedType) -> {
            ApiModelProperty property = findAnnotation(annotatedType, ApiModelProperty.class);
            if (property != null)
            {
                if (StringUtils.hasText(property.value()))
                {
                    schema.setDescription(property.value());
                }
                if (StringUtils.hasText(property.example()))
                {
                    schema.setExample(property.example());
                }
            }
            return schema;
        };
    }

    private Operation customizeLegacyOperation(Operation operation, HandlerMethod handlerMethod)
    {
        Api api = handlerMethod.getBeanType().getAnnotation(Api.class);
        if (api != null)
        {
            List<String> tags = Arrays.stream(api.tags())
                    .filter(StringUtils::hasText)
                    .collect(Collectors.toList());
            if (tags.isEmpty() && StringUtils.hasText(api.value()))
            {
                tags.add(api.value());
            }
            if (!tags.isEmpty())
            {
                operation.setTags(tags);
            }
        }

        ApiOperation apiOperation = handlerMethod.getMethodAnnotation(ApiOperation.class);
        if (apiOperation != null)
        {
            if (StringUtils.hasText(apiOperation.value()))
            {
                operation.setSummary(apiOperation.value());
            }
            if (StringUtils.hasText(apiOperation.notes()))
            {
                operation.setDescription(apiOperation.notes());
            }
        }
        applyLegacyParameterDescriptions(operation, handlerMethod);
        return operation;
    }

    private void applyLegacyParameterDescriptions(Operation operation, HandlerMethod handlerMethod)
    {
        if (operation.getParameters() == null || operation.getParameters().isEmpty())
        {
            return;
        }
        List<Parameter> parameters = operation.getParameters();
        int fallbackIndex = 0;
        for (MethodParameter methodParameter : handlerMethod.getMethodParameters())
        {
            ApiParam apiParam = methodParameter.getParameterAnnotation(ApiParam.class);
            if (apiParam == null)
            {
                continue;
            }
            String parameterName = StringUtils.hasText(apiParam.name())
                    ? apiParam.name() : methodParameter.getParameterName();
            Parameter parameter = parameters.stream()
                    .filter(item -> parameterName != null && parameterName.equals(item.getName()))
                    .findFirst()
                    .orElse(fallbackIndex < parameters.size() ? parameters.get(fallbackIndex++) : null);
            if (parameter != null)
            {
                if (StringUtils.hasText(apiParam.value()))
                {
                    parameter.setDescription(apiParam.value());
                }
                if (apiParam.required())
                {
                    parameter.setRequired(true);
                }
                if (StringUtils.hasText(apiParam.example()))
                {
                    parameter.setExample(apiParam.example());
                }
            }
        }
    }

    private <A extends Annotation> A findAnnotation(AnnotatedType annotatedType, Class<A> annotationType)
    {
        Annotation[] annotations = annotatedType.getCtxAnnotations();
        if (annotations == null)
        {
            return null;
        }
        for (Annotation annotation : annotations)
        {
            if (annotationType.isInstance(annotation))
            {
                return annotationType.cast(annotation);
            }
        }
        return null;
    }
}
