/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.dubbo.admin.controller;

import org.apache.dubbo.admin.annotation.Authority;
import org.apache.dubbo.admin.controller.editors.CustomLocalDateEditor;
import org.apache.dubbo.admin.controller.editors.CustomLocalDateTimeEditor;
import org.apache.dubbo.admin.model.dto.docs.ApiInfoRequest;
import org.apache.dubbo.admin.model.dto.docs.CallDubboServiceRequest;
import org.apache.dubbo.admin.model.dto.docs.CallDubboServiceRequestInterfaceParam;
import org.apache.dubbo.admin.utils.ApiDocsDubboGenericUtil;
import org.apache.dubbo.common.logger.Logger;
import org.apache.dubbo.common.logger.LoggerFactory;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SimplePropertyPreFilter;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.propertyeditors.StringTrimmerEditor;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.PostConstruct;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

/**
 * dubbo doc ui server api.
 */
@Authority(needLogin = true)
@Api(tags = {"dubbo-api-docs-api"})
@RestController
@RequestMapping("/api/{env}/docs")
public class ApiDocsController {

    private static final Logger LOG = LoggerFactory.getLogger(ApiDocsController.class);

    private static final SimplePropertyPreFilter CLASS_NAME_PRE_FILTER = new SimplePropertyPreFilter(HashMap.class);
    static {
        // Remove the "class" attribute from the returned result
        CLASS_NAME_PRE_FILTER.getExcludes().add("class");
    }

    /**
     * retries for dubbo provider
     */
    @Value("${dubbo.consumer.retries:0}")
    private int retries;

    /**
     * timeout
     */
    @Value("${dubbo.consumer.timeout:1000}")
    private int timeout;

    @Value("${dubbo.api.docs.group:apiDocsGroup}")
    private String apiDocsGroup;

    @Value("${dubbo.api.docs.version:v1}")
    private String apiDocsVersion;

    @InitBinder
    public void initBinder(WebDataBinder binder) {
        binder.registerCustomEditor(String.class, new StringTrimmerEditor(true));
        binder.registerCustomEditor(LocalDate.class, new CustomLocalDateEditor());
        binder.registerCustomEditor(LocalDateTime.class, new CustomLocalDateTimeEditor());
    }

    /**
     * Set timeout and retries for {@link ApiDocsDubboGenericUtil}
     * 2020-11-02 11:16:28
     * @param:
     * @return void
     */
    @PostConstruct
    public void setRetriesAndTimeout(){
        ApiDocsDubboGenericUtil.setRetriesAndTimeout(retries, timeout);
    }

    @ApiOperation(value = "request dubbo api", notes = "request dubbo api", httpMethod = "POST", produces = "application/json")
    @PostMapping("/requestDubbo")
    public String callDubboService(CallDubboServiceRequest dubboCfg, @RequestBody List<CallDubboServiceRequestInterfaceParam> methodparams){
        String[] paramTypes = null;
        Object[] paramValues = null;
        if(null != methodparams && !methodparams.isEmpty()){
            paramTypes = new String[methodparams.size()];
            paramValues = new Object[methodparams.size()];
            for(int i = 0; i < methodparams.size(); i++){
                CallDubboServiceRequestInterfaceParam param = methodparams.get(i);
                paramTypes[i] = param.getParamType();
                Object paramValue = param.getParamValue();
                if(isBaseType(param.getParamType()) && null != paramValue){
                    if(paramValue instanceof Map){
                        Map<?, ?> tempMap = (Map<?, ?>) paramValue;
                        if(!tempMap.isEmpty()) {
                            Object tempParamValue = tempMap.values().stream().findFirst().orElse(null);
                            paramValues[i] = this.emptyString2Null(tempParamValue);
                        }
                    } else {
                        paramValues[i] = emptyString2Null(paramValue);
                    }
                } else {
                    this.emptyString2Null(paramValue);
                    paramValues[i] = paramValue;
                }
            }
        }
        if (null == paramTypes) {
            paramTypes = new String[0];
        }
        if (null == paramValues) {
            paramValues = new Object[0];
        }
        try {
            CompletableFuture<Object> future = ApiDocsDubboGenericUtil.invoke(dubboCfg.getRegistryCenterUrl(), dubboCfg.getInterfaceClassName(),
                    dubboCfg.getMethodName(), dubboCfg.isAsync(), dubboCfg.getVersion(), paramTypes, paramValues, dubboCfg.getGroup());
            try {
                Object objResult = future.get();
                return JSON.toJSONString(objResult, CLASS_NAME_PRE_FILTER);
            } catch (InterruptedException | ExecutionException e) {
                LOG.error(e.getMessage(), e);
                return "Some exceptions have occurred, please check the log.";
            }
        } catch (Exception e) {
            return "Error '" + e.getMessage() + "' happened when loading docs from remote. Please make sure api-docs dependency is correctly added to Dubbo providers, check the following link for more instructions: https://cn.dubbo.apache.org/zh-cn/overview/reference/admin/";
        }
    }

    private Object emptyString2Null(Object paramValue){
        if(null != paramValue) {
            if (paramValue instanceof String && StringUtils.isBlank((String) paramValue)) {
                return null;
            } else if (paramValue instanceof Map) {
                Map<String, Object> tempMap = (Map<String, Object>) paramValue;
                tempMap.forEach((k, v) -> {
                    if (v != null && v instanceof String && StringUtils.isBlank((String) v)) {
                        tempMap.put(k, null);
                    } else {
                        this.emptyString2Null(v);
                    }
                });
            }
        }
        return paramValue;
    }

    @ApiOperation(value = "Get basic information of all modules, excluding API parameter information", notes = "Get basic information of all modules, excluding API parameter information", httpMethod = "GET", produces = "application/json")
    @GetMapping("/apiModuleList")
    public String apiModuleList(ApiInfoRequest apiInfoRequest){
        CallDubboServiceRequest req = createCallApiDocsServiceRequest("apiModuleList", apiInfoRequest);

        return callDubboService(req, null);
    }

    @ApiOperation(value = "Get the parameter information of the specified API", notes = "Get the parameter information of the specified API", httpMethod = "GET", produces = "application/json")
    @GetMapping("/apiParamsResp")
    public String apiParamsResp(ApiInfoRequest apiInfoRequest){
        CallDubboServiceRequest req = createCallApiDocsServiceRequest("apiParamsResponseInfo", apiInfoRequest);

        List<CallDubboServiceRequestInterfaceParam> methodparams = new ArrayList<>(1);
        CallDubboServiceRequestInterfaceParam param = new CallDubboServiceRequestInterfaceParam();
        param.setParamType(String.class.getName());
        param.setParamValue(apiInfoRequest.getApiName());
        methodparams.add(param);
        return callDubboService(req, methodparams);
    }

    private static boolean isBaseType(String typeStr) {
        if ("java.lang.Integer".equals(typeStr) ||
                "java.lang.Byte".equals(typeStr) ||
                "java.lang.Long".equals(typeStr) ||
                "java.lang.Double".equals(typeStr) ||
                "java.lang.Float".equals(typeStr) ||
                "java.lang.Character".equals(typeStr) ||
                "java.lang.Short".equals(typeStr) ||
                "java.lang.Boolean".equals(typeStr) ||
                "java.lang.String".equals(typeStr) ||
                "java.math.BigDecimal".equals(typeStr) ||
                "java.math.BigInteger".equals(typeStr)) {
            return true;
        }
        return false;
    }

    private CallDubboServiceRequest createCallApiDocsServiceRequest(String methodName, ApiInfoRequest apiInfoRequest) {
        CallDubboServiceRequest req = new CallDubboServiceRequest();
        req.setRegistryCenterUrl("dubbo://" + apiInfoRequest.getDubboIp() + ":" + apiInfoRequest.getDubboPort());
        req.setMethodName(methodName);

        req.setInterfaceClassName("org.apache.dubbo.apidocs.core.providers.IDubboDocProvider");
        req.setAsync(false);
        req.setVersion(apiDocsVersion);
        req.setGroup(apiDocsGroup);

        return req;
    }
}
