/* (C) 2021 YiRing, Inc. */
package com.yiring.common.swagger;

import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.Console;
import cn.hutool.core.net.NetUtil;
import cn.hutool.core.util.StrUtil;
import io.swagger.v3.oas.models.ExternalDocumentation;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.tags.Tag;
import java.lang.annotation.Annotation;
import java.util.*;
import java.util.stream.Collectors;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.customizers.OpenApiCustomizer;
import org.springdoc.core.customizers.OperationCustomizer;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.core.annotation.Order;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.method.HandlerMethod;

/**
 * Swagger Config
 *
 * @author ifzm
 * @version 0.1
 * 2019/3/11 19:13
 */

@Slf4j
@Order(value = 9999999)
@Profile("!prod")
@Configuration
@RequiredArgsConstructor
public class SwaggerConfig implements CommandLineRunner {

    @Value("${server.address:}")
    String serverAddress;

    @Value("${server.servlet.context-path:}")
    String path;

    @Value("${spring.application.name}")
    String applicationName;

    @Value("${server.port}")
    String port;

    @Getter
    @Value("${app.version}")
    String appVersion;

    @Bean
    public OpenAPI api() {
        Info info = new Info()
            .title("API Doc")
            .description(applicationName)
            .version(appVersion)
            .contact(new Contact().name("© YiRing").url("https://yiring.com").email("developer@yiring.com"));

        return new OpenAPI().info(info).externalDocs(new ExternalDocumentation());
    }

    @Bean(name = "api.any")
    public GroupedOpenApi any() {
        return api("default", List.of("com.yiring"), List.of("/**"), Collections.emptyList());
    }

    @Bean(name = "api.auth")
    public GroupedOpenApi auth() {
        return api("① Auth", List.of("com.yiring.auth.web"), List.of("/**"), List.of("/sys/**"));
    }

    @Bean(name = "api.common")
    public GroupedOpenApi common() {
        return api(
            "② 公共",
            List.of("com.yiring.common.web", "com.yiring.app.web.common"),
            List.of("/**"),
            Collections.emptyList()
        );
    }

    @Bean(name = "api.manage")
    public GroupedOpenApi manage() {
        return api(
            "③ 系统管理",
            List.of("com.yiring.auth.web.sys", "com.yiring.dict.web"),
            List.of("/**"),
            Collections.emptyList()
        );
    }

    @Bean(name = "api.example")
    public GroupedOpenApi example() {
        return api("④ 示例", List.of("com.yiring.app.web.example"), List.of("/**"), Collections.emptyList());
    }

    @Bean
    public OpenApiCustomizer sortTagCustom() {
        String order = "x-order";
        return api -> {
            List<Tag> tags = api
                .getTags()
                .stream()
                .sorted(
                    Comparator.comparing(tag ->
                        Convert.toInt(
                            Optional.ofNullable(tag.getExtensions()).orElseGet(HashMap::new).get(order),
                            Integer.MAX_VALUE
                        )
                    )
                )
                .peek(tag -> {
                    Map<String, Object> extensions = tag.getExtensions();
                    if (Objects.nonNull(extensions) && extensions.containsKey(order)) {
                        extensions.put(order, Convert.toInt(extensions.get(order)));
                    }
                })
                .toList();
            api.setTags(tags);
        };
    }

    /**
     * 自定义 OperationId 用于优化 Pont 接口文件生成
     */
    @Bean
    public OperationCustomizer operationIdCustomizer() {
        return (operation, handlerMethod) -> {
            Class<?> superClazz = handlerMethod.getBeanType().getSuperclass();
            if (Objects.nonNull(superClazz)) {
                String methodType = getType(handlerMethod);

                String beanName = handlerMethod.getBeanType().getSimpleName();
                String methodName = handlerMethod.getMethod().getName();
                operation.setOperationId(String.format("%sUsing%s_%s", methodName, methodType, beanName));
            }
            return operation;
        };
    }

    private static String getType(HandlerMethod handlerMethod) {
        Annotation[] annotations = handlerMethod.getMethod().getAnnotations();
        String methodType = "GET";
        for (Annotation annotation : annotations) {
            if (annotation instanceof PostMapping) {
                methodType = "POST";
            } else if (annotation instanceof PutMapping) {
                methodType = "PUT";
            } else if (annotation instanceof DeleteMapping) {
                methodType = "DELETE";
            }
        }
        return methodType;
    }

    private GroupedOpenApi api(
        String group,
        List<String> basePackages,
        List<String> pathsToMatch,
        List<String> pathsToExclude
    ) {
        return GroupedOpenApi
            .builder()
            .group(group)
            .packagesToScan(basePackages.toArray(new String[0]))
            .addOpenApiMethodFilter(method -> method.isAnnotationPresent(io.swagger.v3.oas.annotations.Operation.class))
            .pathsToMatch(pathsToMatch.toArray(new String[0]))
            .pathsToExclude(pathsToExclude.toArray(new String[0]))
            .addOperationCustomizer(operationIdCustomizer())
            .build();
    }

    @Override
    public void run(String... args) {
        LinkedHashSet<String> ips = new LinkedHashSet<>();
        if (StrUtil.isBlank(serverAddress)) {
            ips.addAll(NetUtil.localIpv4s());
        } else {
            ips.add(serverAddress);
        }

        String protocol = "http";
        String link = ips
            .stream()
            .map(host -> "> " + protocol + "://" + host + ":" + port + path + "/doc.html")
            .collect(Collectors.joining("\n\t\t"));
        Console.log("\n\t📖 API Doc (OpenAPI 3): \n\t\t{}\n", link);
    }
}
