package com.ruoyi.web.core.config;


import cn.hutool.core.collection.CollectionUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONWriter;
import com.fasterxml.classmate.ResolvedType;
import com.fasterxml.classmate.TypeResolver;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.BaseEntity;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.utils.ContainerUtils;
import com.ruoyi.common.utils.StringUtils;
import io.swagger.annotations.ApiModelProperty;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import springfox.documentation.builders.ExampleBuilder;
import springfox.documentation.builders.ResponseBuilder;
import springfox.documentation.schema.Example;
import springfox.documentation.service.ResolvedMethodParameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.ResponseBuilderPlugin;
import springfox.documentation.spi.service.contexts.OperationContext;
import springfox.documentation.spi.service.contexts.ResponseContext;
import springfox.documentation.swagger.common.SwaggerPluginSupport;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

// Swagger参数字段类型映射插件 zhao
@Configuration
@Order(SwaggerPluginSupport.SWAGGER_PLUGIN_ORDER + 1002)
public class SwaggerParameterPlugin implements ResponseBuilderPlugin
{
    @Autowired
    private TypeResolver typeResolver;

    private static final String DEFAULT_QUERY = "* Query";
    private static final String DEFAULT_FROM = "* FromData";
    private static final String DEFAULT_JSON = "* Json";
    private static final String DEFAULT_PATH = "* 路径参数";
    private static final String DEFAULT_ROOT_NAME = "请求参数";

    @Override
    public void apply(ResponseContext responseContext)
    {
        try
        {
            ResolvedType returnType = responseContext.getOperationContext().getReturnType();
            Class<?> type = returnType.getErasedType();

            if(TableDataInfo.class.equals(type))
            {
                HandleReturnObject_TableDataInfo(responseContext);
            }
            else
            {
                HandleReturnObject(responseContext);
            }
        }
        catch(Exception e)
        {
            e.printStackTrace();
        }
    }

    @Override
    public boolean supports(DocumentationType documentationType)
    {
        return true;
    }

    private List<ParameterTypeMap> GetAnnotations(ResponseContext responseContext)
    {
        OperationContext operationContext = responseContext.getOperationContext();
        List<ParameterTypeMap> res = new ArrayList<>();

        Optional<ParameterTypeMap> annotation = operationContext.findAnnotation(ParameterTypeMap.class);
        annotation.ifPresent(res::add);

        Optional<ParameterTypeMapGroup> annotations = operationContext.findAnnotation(ParameterTypeMapGroup.class);
        annotations.ifPresent(responseTypeMaps -> {
            res.addAll(Arrays.asList(responseTypeMaps.value()));
        });
        return res;
    }

    private void HandleReturnObject(ResponseContext responseContext)
    {
        Map<String, List<ResolvedMethodParameter>> group = GetParmsGroup(responseContext);

        if(group.values().stream().allMatch(CollectionUtil::isEmpty))
            return;

        List<ParameterTypeMap> responseTypeMaps = GetAnnotations(responseContext);
        Map<String, List<ParameterTypeMap>> ruleGroup = responseTypeMaps.stream().collect(Collectors.groupingBy(ParameterTypeMap::paramType));

        HandlePath(responseContext, group.get(ParameterTypeMap.PARAM_TYPE_PATH), ruleGroup.getOrDefault(ParameterTypeMap.PARAM_TYPE_PATH, new ArrayList<>()));
        HandleQuery(responseContext, group.get(ParameterTypeMap.PARAM_TYPE_QUERY), ruleGroup.getOrDefault(ParameterTypeMap.PARAM_TYPE_QUERY, new ArrayList<>()));
        HandleJson(responseContext, group.get(ParameterTypeMap.PARAM_TYPE_JSON), ruleGroup.get(ParameterTypeMap.PARAM_TYPE_JSON));
    }

    private void HandleReturnObject_TableDataInfo(ResponseContext responseContext)
    {
        List<ParameterTypeMap> responseTypeMaps = GetAnnotations(responseContext);
        Map<String, List<ResolvedMethodParameter>> group = GetParmsGroup(responseContext);

        HandlePath(responseContext, group.get(ParameterTypeMap.PARAM_TYPE_PATH), responseTypeMaps);
        HandleQuery(responseContext, group.get(ParameterTypeMap.PARAM_TYPE_QUERY), responseTypeMaps,
                buildExample("pageNum", "页码", int.class, false, "不分页"),
                buildExample("pageSize", "页大小", int.class, false, "不分页"),
                buildExample("orderByColumn", "排序字段(下划线小写拼接名)", String.class, false, "不主动排序"),
                buildExample("isAsc", "排序顺序: asc=正序, desc=倒序", String.class, false, "正序"),
                buildExample("translate_dict", "是否翻译字段的字典", boolean.class, false, "0"),
                buildExample("translate_dict_backup_raw", "翻译字段的字典时备份原值到params对象中", boolean.class, false, "0")
                );
    }

    private String ClassToJson(Class<?> type, Class<?>...genericType)
    {
        return SwaggerDocUtils.ToVisualJson(GetClassFields(type, genericType));
    }

    private Object GetClassFields(Class<?> clazz, Class<?>...genericType)
    {
        if(Collection.class.isAssignableFrom(clazz))
        {
            List<Object> list = new ArrayList<>();
            Class<?> c;
            if(null != genericType && genericType.length > 0)
            {
                c = genericType[0];
                Map<String, Object> map = new LinkedHashMap<>();
                FetchClassFields(c, map, new HashSet<>());
                if(!map.isEmpty())
                    list.add(map);
                else
                {
                    list.add("数据对象1 [" + c.getSimpleName() + "]");
                    list.add("数据对象2 [" + c.getSimpleName() + "]");
                    list.add("数据对象3 [" + c.getSimpleName() + "]");
                    list.add("数据对象 ......");
                }
            }
            else
            {
                list.add("数据对象1 [Any]");
                list.add("数据对象2 [Any]");
                list.add("数据对象3 [Any]");
                list.add("数据对象 ......");
            }
            return list;
        }
        else if(SwaggerDocUtils.IgnoreClass(clazz))
        {
            return SwaggerDocUtils.ConvJSTypeName(clazz);
        }
        else
        {
            Map<String, Object> map = new LinkedHashMap<>();
            FetchClassFields(clazz, map, new HashSet<>());
            return map;
        }
    }

    private void FetchClassFields(Class<?> clazz, Map<String, Object> map, Set<Field> ignoreClass)
    {
        if(SwaggerDocUtils.IgnoreClass(clazz))
            return;

        Field[] declaredFields = clazz.getDeclaredFields();
        for(Field field : declaredFields)
        {
            String name = field.getName();
            if(map.containsKey(name))
                continue; // 基类不覆盖派生类

            if(SwaggerDocUtils.IgnoreField(field))
                continue;

            Class<?> t = field.getType();

            if(!ignoreClass.contains(field) && Collection.class.isAssignableFrom(t))
            {
                Set<Field> ic = new HashSet<>();
                ic.addAll(ignoreClass);
                ic.add(field);
                map.put(name, Collections.singletonList(FetchCollectionFields(field.getGenericType(), ic)));
                continue;
            }

            if(!ignoreClass.contains(field) && BaseEntity.class.isAssignableFrom(t))
            {
                Set<Field> ic = new HashSet<>();
                ic.addAll(ignoreClass);
                ic.add(field);
                Map<String, Object> m = new LinkedHashMap<>();
                FetchClassFields(t, m, ic);
                map.put(name, m);
                continue;
            }

            String desc = "";
            String type = SwaggerDocUtils.ConvJSTypeName(t);
            String what = "";
            Excel annotation = field.getAnnotation(Excel.class);
            if(null != annotation)
            {
                what = annotation.name();
                String dictType = annotation.dictType();
                if(StringUtils.isNotEmpty(dictType))
                    desc += "字典:" + dictType + " ";
                String readConverterExp = annotation.readConverterExp();
                if(StringUtils.isNotEmpty(readConverterExp))
                    desc += "转换表达式:" + readConverterExp + " ";
                String dateFormat = annotation.dateFormat();
                if(StringUtils.isNotEmpty(dateFormat))
                    desc += "日期格式:" + dateFormat + " ";
            }
            ApiModelProperty apiModelProperty = field.getAnnotation(ApiModelProperty.class);
            if(null != apiModelProperty)
            {
                what = apiModelProperty.value();
                desc += apiModelProperty.required() ? "必填" : "可选";
                desc += " ";
            }

            String value = desc + " [" + what + " " +  type + "]";
            map.put(name, value);
        }

        Class<?> superclass = clazz.getSuperclass();

        FetchClassFields(superclass, map, ignoreClass);
    }

    private Object FetchCollectionFields(Type genericType, Set<Field> ignoreClass)
    {
        Map<String, Object> map = new LinkedHashMap<>();
        if (genericType instanceof ParameterizedType) {
            ParameterizedType ptype = (ParameterizedType)genericType;
            Type[] actualTypeArguments = ptype.getActualTypeArguments();
            if(null != actualTypeArguments && actualTypeArguments.length > 0)
            {
                Type actualTypeArgument = actualTypeArguments[0];
                if(actualTypeArgument instanceof Class<?>)
                {
                    Class<?> c = (Class<?>) actualTypeArgument;
                    FetchClassFields(c, map, ignoreClass);
                }
                else if(actualTypeArgument instanceof ParameterizedType)
                {
                    ParameterizedType pt = (ParameterizedType)actualTypeArgument;
                    Type rawType = pt.getRawType();
                    if(rawType instanceof Class<?> && Collection.class.isAssignableFrom((Class<?>)rawType))
                    {
                        List<Object> list = new ArrayList<>();
                        list.add(FetchCollectionFields(pt, ignoreClass));
                        return list;
                    }
                }
            }
        }
        return map;
    }

    private String buildExample(ParameterTypeMap parm)
    {
        return buildExample(parm.name(), parm.desc(), parm.type(), parm.required(), parm.defaultValue());
    }

    private static String buildExample(String name, String desc, Class<?> type, boolean required, String defaultValue)
    {
        StringBuilder builder = new StringBuilder();

        builder.append(name).append(": ");
        builder.append(null != defaultValue ? "默认" + defaultValue : "")
                    .append(" ")
                    .append(SwaggerDocUtils.ConvJSTypeName(type))
                    .append(" ")
                    .append(required ? "必填" : "可选")
                .append(" ")
                .append(desc)
                ;

        return builder.toString();
    }

    private void HandleQuery(ResponseContext responseContext, List<ResolvedMethodParameter> parms, List<ParameterTypeMap> parameterTypeMaps, String...args)
    {
        List<String> list = new ArrayList<>();
        if(null != args && args.length > 0)
        {
            list.addAll(Arrays.asList(args));
        }
        if(CollectionUtil.isEmpty(list) && CollectionUtil.isEmpty(parms))
            return;

        Map<String, ParameterTypeMap> parmMap = parameterTypeMaps.stream().filter((x) -> StringUtils.isNotEmpty(x.name())).collect(Collectors.toMap(ParameterTypeMap::name, x -> x, (a, b) -> a));

        parms.forEach((x) -> {
            Class<?> type = x.getParameterType().getErasedType();
            if(BaseEntity.class.isAssignableFrom(type))
            {
                List<Parm> strings = GetClassParms(type);
                strings.forEach((y) -> {
                    SetupParm(y, parmMap.get(y.name));
                    list.add(y.toString());
                });
            }
            else
            {
                Parm parm = ToParm(x);
                list.add(SetupParm(parm, parmMap.get(parm.name)).toString());
            }
        });

        ExampleBuilder builder = new ExampleBuilder()
                .summary(DEFAULT_QUERY)
                .value(String.join("\n", list))
                .mediaType(DEFAULT_ROOT_NAME)
                .id(ParameterTypeMap.PARAM_TYPE_QUERY)
                ;
        responseContext.responseBuilder().examples(Collections.singletonList(builder.build()));
    }

    private void HandlePath(ResponseContext responseContext, List<ResolvedMethodParameter> parms, List<ParameterTypeMap> parameterTypeMaps)
    {
        if(CollectionUtil.isEmpty(parms))
            return;

        List<String> list = new ArrayList<>();
        Map<String, ParameterTypeMap> parmMap = parameterTypeMaps.stream().filter((x) -> StringUtils.isNotEmpty(x.name())).collect(Collectors.toMap(ParameterTypeMap::name, x -> x, (a, b) -> a));

        parms.forEach((x) -> {
            Class<?> type = x.getParameterType().getErasedType();
            if(BaseEntity.class.isAssignableFrom(type))
            {
                List<Parm> strings = GetClassParms(type);
                strings.forEach((y) -> {
                    SetupParm(y, parmMap.get(y.name));
                    list.add(y.toString());
                });
            }
            else
            {
                Parm parm = ToParm(x);
                list.add(SetupParm(parm, parmMap.get(parm.name)).toString());
            }
        });

        ExampleBuilder builder = new ExampleBuilder()
                .summary(DEFAULT_PATH)
                .value(String.join("\n", list))
                .mediaType(DEFAULT_ROOT_NAME)
                .id(ParameterTypeMap.PARAM_TYPE_PATH)
                ;
        responseContext.responseBuilder().examples(Collections.singletonList(builder.build()));
    }

    private void HandleJson(ResponseContext responseContext, List<ResolvedMethodParameter> parms, List<ParameterTypeMap> parameterTypeMaps)
    {
        if(CollectionUtil.isEmpty(parms))
            return;

        parms.forEach((x) -> {
            Class<?> type = x.getParameterType().getErasedType();
            Object value = GetClassFields(type);
            if(!(value instanceof String))
                value = SwaggerDocUtils.ToVisualJson(value);

            ExampleBuilder builder = new ExampleBuilder()
                    .summary(DEFAULT_JSON)
                    .value(value)
                    .mediaType(DEFAULT_ROOT_NAME)
                    .id(ParameterTypeMap.PARAM_TYPE_JSON)
                    ;
            responseContext.responseBuilder().examples(Collections.singletonList(builder.build()));
        });
    }

    private List<Parm> GetClassParms(Class<?> clazz)
    {
        List<Parm> res = new ArrayList<>();
        FetchClassParms(clazz, res);
        return res;
    }

    private void FetchClassParms(Class<?> clazz, List<Parm> ret)
    {
        if(SwaggerDocUtils.IgnoreClass(clazz))
            return;

        Field[] declaredFields = clazz.getDeclaredFields();
        for(Field field : declaredFields)
        {
            String name = field.getName();
            if(ret.stream().anyMatch((x) -> name.equals(x.name)))
                continue; // 基类不覆盖派生类

            if(SwaggerDocUtils.IgnoreField(field))
                continue;

            Class<?> t = field.getType();
            Parm parm = new Parm();

            String desc = "";
            String defaultValue = "";
            String what = "";
            Excel annotation = field.getAnnotation(Excel.class);
            if(null != annotation)
            {
                what = annotation.name();
                String dictType = annotation.dictType();
                if(StringUtils.isNotEmpty(dictType))
                    desc += "字典:" + dictType + " ";
                String readConverterExp = annotation.readConverterExp();
                if(StringUtils.isNotEmpty(readConverterExp))
                    desc += "转换表达式:" + readConverterExp + " ";
                String dateFormat = annotation.dateFormat();
                if(StringUtils.isNotEmpty(dateFormat))
                    desc += "日期格式:" + dateFormat + " ";

                defaultValue = annotation.defaultValue();
            }
            ApiModelProperty apiModelProperty = field.getAnnotation(ApiModelProperty.class);
            if(null != apiModelProperty)
            {
                what = apiModelProperty.value();
                desc += apiModelProperty.required() ? "必填" : "可选";
                desc += " ";
            }

            String value = desc;
            parm.name = name;
            parm.desc = what + " " + value;
            parm.defaultValue = defaultValue;
            parm.type = t;
            ret.add(parm);
        }

        Class<?> superclass = clazz.getSuperclass();

        FetchClassParms(superclass, ret);
    }

    private Map<String, List<ResolvedMethodParameter>> GetParmsGroup(ResponseContext responseContext)
    {
        List<ResolvedMethodParameter> parameters = responseContext.getOperationContext().getParameters();
        Map<String, List<ResolvedMethodParameter>> group = new HashMap<>();
        group.put(ParameterTypeMap.PARAM_TYPE_QUERY, new ArrayList<>());
        group.put(ParameterTypeMap.PARAM_TYPE_JSON, new ArrayList<>());
        group.put(ParameterTypeMap.PARAM_TYPE_PATH, new ArrayList<>());
        parameters.forEach((x) -> {
            if(x.hasParameterAnnotation(RequestBody.class))
                group.get(ParameterTypeMap.PARAM_TYPE_JSON).add(x);
            else if(x.hasParameterAnnotation(PathVariable.class))
                group.get(ParameterTypeMap.PARAM_TYPE_PATH).add(x);
            else
                group.get(ParameterTypeMap.PARAM_TYPE_QUERY).add(x);
        });
        return group;
    }

    private List<ResolvedMethodParameter> GetParmsList(ResponseContext responseContext, String type)
    {
        List<ResolvedMethodParameter> parameters = responseContext.getOperationContext().getParameters();
        List<ResolvedMethodParameter> res = new ArrayList<>();
        parameters.forEach((x) -> {
            if(x.hasParameterAnnotation(RequestBody.class) && ParameterTypeMap.PARAM_TYPE_JSON.equals(type))
                res.add(x);
            else if(x.hasParameterAnnotation(PathVariable.class) && ParameterTypeMap.PARAM_TYPE_PATH.equals(type))
                res.add(x);
            else
                res.add(x);
        });
        return res;
    }

    private static class Parm
    {
        String name;
        String desc;
        Class<?> type;
        String defaultValue;
        boolean required;

        @Override
        public String toString()
        {
            return buildExample(name, desc, type, required, defaultValue);
        }
    }

    private Parm SetupParm(Parm parm, ParameterTypeMap rule)
    {
        if(null == rule)
            return parm;
        if(!parm.name.equals(rule.name()))
            return parm;
        if(StringUtils.isNotEmpty(rule.defaultValue()))
            parm.defaultValue = rule.defaultValue();
        if(StringUtils.isNotEmpty(rule.desc()))
            parm.desc = (null != parm.desc ? parm.desc : "") + rule.desc();
        if(!parm.required)
            parm.required = rule.required();
        return parm;
    }

    private Parm ToParm(ResolvedMethodParameter parm)
    {
        Parm p = new Parm();
        Optional<RequestParam> requestParam = parm.findAnnotation(RequestParam.class);
        //Optional<PathVariable> pathVariable = parm.findAnnotation(PathVariable.class);
        p.required = parm.hasParameterAnnotation(PathVariable.class) || (requestParam.isPresent() && requestParam.get().required());
        p.name = parm.defaultName().orElse("");
        p.defaultValue = requestParam.map(RequestParam::defaultValue).orElse("");
        p.type = parm.getParameterType().getErasedType();
        p.desc = "";
        return p;
    }
}
