package cn.willingxyz.restdoc.swagger3;

import cn.willingxyz.restdoc.core.parse.utils.EnumSerializer;
import cn.willingxyz.restdoc.core.parse.utils.FormatUtils;
import cn.willingxyz.restdoc.core.parse.utils.ReflectUtils;
import cn.willingxyz.restdoc.core.parse.utils.TextUtils;
import cn.willingxyz.restdoc.swagger.common.utils.ClassNameUtils;
import cn.willingxyz.restdoc.swagger.common.utils.JsonUtils;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.github.therapi.runtimejavadoc.RuntimeJavadoc;
import cn.willingxyz.restdoc.core.models.*;
import cn.willingxyz.restdoc.core.parse.IRestDocGenerator;
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.PathItem;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.media.*;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.parameters.RequestBody;
import io.swagger.v3.oas.models.responses.ApiResponse;
import io.swagger.v3.oas.models.responses.ApiResponses;
import io.swagger.v3.oas.models.servers.Server;
import io.swagger.v3.oas.models.tags.Tag;
import lombok.var;

import java.lang.reflect.Type;
import java.util.*;
import java.util.stream.Collectors;

import static cn.willingxyz.restdoc.swagger.common.utils.StringUtils.combineStr;

public class Swagger3RestDocGenerator implements IRestDocGenerator {

    public Swagger3GeneratorConfig _config;

    public Swagger3RestDocGenerator(Swagger3GeneratorConfig configuration) {
        _config = configuration;
    }

    @Override
    public String generate(RootModel rootModel) {
        var openApi = generateOpenApi(rootModel);

        if (_config.getOpenAPIFilters() != null) {
            for (IOpenAPIFilter openAPIFilter : _config.getOpenAPIFilters()) {
                openApi = openAPIFilter.handle(openApi);
            }
        }

        var objectMapper = JsonUtils.objectMapper();
        try {
            var swaggerJson = objectMapper.writeValueAsString(openApi);
            return swaggerJson;
        } catch (JsonProcessingException e) {
            throw new RuntimeException("序列化错误");
        }
    }

    private OpenAPI generateOpenApi(RootModel rootModel) {
        var openApi = new OpenAPI();

        convertServers(rootModel, openApi);
        convertInfo(rootModel, openApi);
        convertTag(rootModel, openApi);
        convertPath(rootModel, openApi);

        return openApi;
    }

    private void convertServers(RootModel rootModel, OpenAPI openApi) {
        var servers = new ArrayList<Server>();
        for (var server : _config.getServers()) {
            var serverInfo = new Server();
            serverInfo.setDescription(server.getDescription());
            serverInfo.setUrl(server.getUrl());

            servers.add(serverInfo);
        }
        openApi.setServers(servers);
    }

    private void convertInfo(RootModel rootModel, OpenAPI openApi) {
        var info = new Info();
        info.setDescription(_config.getDescription());
        info.setVersion(_config.getVersion());
        info.setTitle(_config.getTitle());
        openApi.setInfo(info);
    }


    // tag 用于对path进行分组,类似于springmvc的controller
    private void convertTag(RootModel rootModel, OpenAPI openApi) {
        for (var controller : rootModel.getControllers()) {
            var tag = new Tag();
            tag.setName(getTagName(controller));
            if (!_config.isResolveJavaDocAsTypeName())
                tag.setDescription(controller.getDescription());
            openApi.addTagsItem(tag);
        }
    }

    private void convertPath(RootModel rootModel, OpenAPI openApi) {
        for (var controller : rootModel.getControllers()) {
            for (var method : controller.getControllerMethods()) {
                for (var mapping : method.getMappings()) {
                    convertSinglePath(openApi, controller, method, mapping);
                }
            }
        }
    }

    private void convertSinglePath(OpenAPI openApi, ControllerModel controller, PathModel method, MappingModel mapping) {


        var operation = new Operation();
        operation.addTagsItem(getTagName(controller));
        operation.setSummary(TextUtils.getFirstLine(method.getDescription()));
        operation.setDescription(method.getDescription());
        operation.setDeprecated(method.getDeprecated());

        // 参数解析
        for (var param : method.getParameters()) {
            if (param.getLocation() == ParameterModel.ParameterLocation.QUERY) {
                convertQueryString(param, openApi).forEach(o -> operation.addParametersItem(o));
            } else if (param.getLocation() == ParameterModel.ParameterLocation.BODY) {
                operation.setRequestBody(convertRequestBody(param, openApi));
            } else if (param.getLocation() == ParameterModel.ParameterLocation.PATH) {
                operation.addParametersItem(generateSingleParameterSchema("path", param, openApi));
            } else if (param.getLocation() == ParameterModel.ParameterLocation.HEADER) {
                operation.addParametersItem(generateSingleParameterSchema("header", param, openApi));
            } else if (param.getLocation() == ParameterModel.ParameterLocation.FILE) {
                operation.setRequestBody(convertFileParameter(param));
            }
        }
        // 响应解析
        operation.setResponses(convertResponses(method, openApi));

        for (var path : mapping.getPaths()) {
            PathItem pathItem = null;
            if (openApi.getPaths() != null) {
                pathItem = openApi.getPaths().entrySet().stream()
                        .filter(o -> o.getKey().equals(path))
                        .map(o -> o.getValue())
                        .findFirst()
                        .orElse(null);
            }
            if (pathItem == null)
                pathItem = new PathItem();
            pathItem.setDescription(method.getDescription());
            // summary 属于简短的描述
            pathItem.setSummary(TextUtils.getFirstLine(method.getDescription()));
            setHttpMethod(mapping, pathItem, operation);

            openApi.path(path, pathItem);
        }
    }

    private void setHttpMethod(MappingModel mapping, PathItem pathItem, Operation operation) {
        for (var httpMethod : mapping.getHttpMethods()) {
            switch (httpMethod) {
                case GET:
                    pathItem.get(operation);
                    break;
                case PUT:
                    pathItem.put(operation);
                    break;
                case POST:
                    pathItem.post(operation);
                    break;
                case DELETE:
                    pathItem.delete(operation);
                    break;
                case HEAD:
                    pathItem.head(operation);
                    break;
                case PATCH:
                    pathItem.patch(operation);
                    break;
                case TRACE:
                    pathItem.trace(operation);
                    break;
                case OPTIONS:
                    pathItem.options(operation);
                    break;
            }
        }
    }

    /**
     * 转换RequestBody
     */
    private RequestBody convertRequestBody(ParameterModel parameterModel, OpenAPI openAPI) {
        var requestBody = new RequestBody();
        requestBody.setRequired(parameterModel.isRequired());
        requestBody.setDescription(parameterModel.getDescription());

        Schema contentSchema = generateSchema(parameterModel.getEnums(), parameterModel.getDescription(), parameterModel.getParameterType(), parameterModel.getChildren(), openAPI);

        requestBody.setContent(createContent(contentSchema));
        return requestBody;
    }

    private List<Parameter> convertQueryString(ParameterModel paramModel, OpenAPI openAPI) {
        var parameters = new ArrayList<Parameter>();
        if (paramModel.getChildren() != null && paramModel.getChildren().size() > 0) // 复杂对象
        {
            convertParameterChildren(paramModel.getChildren(), null, openAPI)
                    .stream().forEach(o -> parameters.add(o));
        } else {
            parameters.add(generateSingleParameterSchema("query", paramModel, openAPI));
        }
        return parameters;
    }

    private RequestBody convertFileParameter(ParameterModel param) {
        var requestBody = new RequestBody();

        var propSchema = new Schema();
        propSchema.setType("string");
        propSchema.format("binary");

        Schema schema = new Schema();
        schema.setType("object");
        schema.addProperties(param.getName(), propSchema);

        Content content = new Content();
        var mediaType = new MediaType();
        mediaType.setSchema(schema);
        content.addMediaType("multipart/form-data", mediaType);

        requestBody.setContent(content);
        requestBody.setDescription(param.getDescription());
        requestBody.setRequired(param.isRequired());

        return requestBody;
    }

    private ApiResponse convertResponse(ResponseModel responseModel, OpenAPI openAPI) {
        var apiResponse = new ApiResponse();
        var returnModel = responseModel.getReturnModel();

        apiResponse.setDescription(returnModel.getDescription());
        if (returnModel.getReturnType() == void.class || returnModel.getReturnType() == Void.class)
            return apiResponse;

        Schema schema = generateSchema(returnModel.getEnums(), returnModel.getDescription(), returnModel.getReturnType(), returnModel.getChildren(), openAPI);

        apiResponse.setContent(createContent(schema));
        return apiResponse;
    }

    private ApiResponses convertResponses(PathModel method, OpenAPI openAPI) {
        var response = new ApiResponses();
        for (var res : method.getResponse()) {
            ApiResponse apiResponse = convertResponse(res, openAPI);
            response.addApiResponse(res.getStatusCode() + "", apiResponse);
        }
        return response;
    }

    private Content createContent(Schema schema) {
        var mediaType = new MediaType();
        mediaType.setSchema(schema);
        var content = new Content();
        content.addMediaType("application/json", mediaType);
        return content;
    }

    private Parameter generateSingleParameterSchema(String in, ParameterModel paramModel, OpenAPI openAPI) {
        var parameter = new Parameter();
        parameter.setName(paramModel.getName());
        parameter.setDescription(paramModel.getDescription());
        parameter.setIn(in);
        parameter.setRequired(paramModel.isRequired());

        Schema schema = generateSchema(paramModel.getEnums(), paramModel.getDescription(), paramModel.getParameterType(), paramModel.getChildren(), openAPI);
        parameter.setSchema(schema);
        parameter.setDescription(combineStr(parameter.getDescription(), schema.getDescription()));
        return parameter;
    }

    private List<Parameter> convertParameterChildren(List<PropertyModel> propertyModels, String paraName, OpenAPI openAPI) {
        var parameters = new ArrayList<Parameter>();
        String name = "";
        if (paraName != null)
            name = paraName + ".";
        for (var child : propertyModels) {
            if (child.isArray()) {
                convertParameterChildren(child.getChildren(), child.getName(), openAPI).stream().forEach(o -> parameters.add(o));
            } else if (child.getChildren() == null || child.getChildren().size() == 0) {
                var parameter = new Parameter();

                parameter.setName(name + child.getName());
                parameter.setDescription(child.getDescription());
                parameter.setRequired(child.isRequired());
                parameter.setIn("query");

                Schema schema = generateSchema(child.getEnums(), child.getDescription(), child.getPropertyType(), child.getChildren(), openAPI);
                parameter.setSchema(schema);
                parameter.setDescription(combineStr(parameter.getDescription(), schema.getDescription()));

                parameters.add(parameter);
            } else {
                parameters.addAll(convertParameterChildren(child.getChildren(), name + child.getName(), openAPI));
            }
            // todo path
        }
        return parameters;
    }

    // ----------------------core---------------

    // 生成Schema,并放入openapi中。
    private Schema getOrGenerateComplexSchema(List<String> enums, Type type, List<PropertyModel> children, OpenAPI openAPI) {
        var componentName = ClassNameUtils.getComponentName(_config.getTypeInspector(), _config.getTypeNameParser(), type);

        if (openAPI.getComponents() == null)
            openAPI.components(new Components());
        if (openAPI.getComponents().getSchemas() == null)
            openAPI.getComponents().schemas(new LinkedHashMap<>());
        if (!openAPI.getComponents().getSchemas().containsKey(componentName)) {
            var schema = generateComplexTypeSchema(enums, type, children, openAPI);
            schema.setRequired(children.stream().filter(o -> o.isRequired()).map(o -> o.getName()).collect(Collectors.toList()));
            openAPI.getComponents().addSchemas(componentName, schema);
        }
        return openAPI.getComponents().getSchemas().get(componentName);
    }

    // 生成schema
    private Schema generateSchema(List<String> enums, String description, Type type, List<PropertyModel> children, OpenAPI openAPI) {
        // 数组/集合
        if (_config.getTypeInspector().isCollection(type)) {
            return generateArraySchema(enums, description, type, children, openAPI);
        }
        // 枚举
        if (ReflectUtils.isEnum(type)) {
            return generateEnumSchema((Class) type, enums, description);
        }
        // 简单类型
        if (children == null || children.isEmpty()) {
            return generateSimpleTypeSchema(description, type);
        }
        // 复杂类型
        return getOrGenerateComplexSchema(enums, type, children, openAPI);
    }

    private Schema generateArraySchema(List<String> enums, String description, Type type, List<PropertyModel> children, OpenAPI openAPI) {
        var arraySchema = new ArraySchema();
        arraySchema.setDescription(description);
        Schema schema = generateSchema(enums, description, _config.getTypeInspector().getCollectionComponentType(type), children, openAPI);
        arraySchema.setItems(schema);
        arraySchema.setDescription(combineStr(arraySchema.getDescription(), schema.getDescription()));
        return arraySchema;
    }

    private Schema generateComplexTypeSchema(List<String> enums, Type type, List<PropertyModel> children, OpenAPI openAPI) {
        var schema = new Schema();
        var classDoc = RuntimeJavadoc.getJavadoc(type.getTypeName());
        schema.setDescription(FormatUtils.format(classDoc.getComment()));
        schema.setProperties(generateComplexTypeSchemaProperty(enums, type, children, openAPI));

        return schema;
    }

    private Map<String, Schema> generateComplexTypeSchemaProperty(List<String> enums, Type type, List<PropertyModel> propertyModels, OpenAPI openAPI) {
        var schemas = new LinkedHashMap<String, Schema>();

        for (var propertyModel : propertyModels) {
            var schema = generateSchema(enums, propertyModel.getDescription(), propertyModel.getPropertyType(), propertyModel.getChildren(), openAPI);
            schema.setExample(propertyModel.getExample());
//            schema.setRequired(propertyModels.stream().filter(o -> o.isRequired()).map(o -> o.getName()).collect(Collectors.toList()));
            schemas.put(propertyModel.getName(), schema);
        }
        return schemas;
    }

    private Schema generateSimpleTypeSchema(String description, Type type) {
        var schema = new Schema();
        schema.setDescription(description);
        schema.setType(_config.getSwaggerTypeInspector().toSwaggerType(type));
        schema.setFormat(_config.getSwaggerTypeInspector().toSwaggerFormat(type));
        return schema;
    }

    private Schema generateEnumSchema(Class clazz, List<String> enums, String description) {
        var itemSchema = new Schema();
        itemSchema.setType("string"); // todo 如何决定是string还是int
        itemSchema.setEnum(enums);
        itemSchema.setDescription(description);
        return itemSchema;
    }

    private String getTagName(ControllerModel controller) {
        return _config.getTypeNameParser().parse(controller.getControllerClass());
    }
}