/*
 *  Copyright (c) 2020, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 *
 *  WSO2 Inc. 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.wso2.apimgt.gateway.cli.protobuf;

import com.google.protobuf.DescriptorProtos;
import com.google.protobuf.ExtensionRegistry;
import io.swagger.v3.oas.models.OpenAPI;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.wso2.apimgt.gateway.cli.exception.CLICompileTimeException;
import org.wso2.apimgt.gateway.cli.exception.CLIInternalException;
import org.wso2.apimgt.gateway.cli.exception.CLIRuntimeException;
import org.wso2.apimgt.gateway.cli.model.route.EndpointListRouteDTO;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

/**
 * Class for generate file descriptors for proto files and create OpenAPI objects out of those descriptors.
 */
public class ProtobufParser {
    private static final Logger logger = LoggerFactory.getLogger(ProtobufParser.class);
    /**
     * Compile the protobuf and generate descriptor file.
     *
     * @param protoPath      protobuf file path
     * @param descriptorPath descriptor file path
     * @return {@link DescriptorProtos.FileDescriptorSet} object
     */
    private static DescriptorProtos.FileDescriptorProto generateRootFileDescriptor(String protoPath,
                                                                                  String descriptorPath) {
        String command = new ProtocCommandBuilder
                (protoPath, resolveProtoFolderPath(protoPath), descriptorPath).build();
        generateDescriptor(command);
        File initialFile = new File(descriptorPath);
        try (InputStream targetStream = new FileInputStream(initialFile)) {
            ExtensionRegistry extensionRegistry = ExtensionRegistry.newInstance();
            //to register all custom extensions in order to parse properly
            ExtensionHolder.registerAllExtensions(extensionRegistry);
            DescriptorProtos.FileDescriptorSet set = DescriptorProtos.FileDescriptorSet.parseFrom(targetStream,
                    extensionRegistry);
            logger.debug("Descriptor file is parsed successfully. file:" , descriptorPath);
            if (set.getFileList().size() > 0) {
                return set.getFile(0);
            }
        } catch (IOException e) {
            throw new CLIInternalException("Error reading generated descriptor file '" + descriptorPath + "'.", e);
        }
        return null;
    }

    /**
     * Execute command and generate file descriptor.
     *
     * @param command protoc executor command.
     */
    private static void generateDescriptor(String command) {
        boolean isWindows = System.getProperty("os.name")
                .toLowerCase(Locale.ENGLISH).startsWith("windows");
        ProcessBuilder builder = new ProcessBuilder();
        if (isWindows) {
            builder.command("cmd.exe", "/c", command);
        } else {
            builder.command("sh", "-c", command);
        }
        builder.directory(new File(System.getProperty("user.home")));
        Process process;
        try {
            process = builder.start();
        } catch (IOException e) {
            throw new CLIInternalException("Error in executing protoc command '" + command + "'.", e);
        }
        try {
            process.waitFor();
        } catch (InterruptedException e) {
            throw new CLIRuntimeException("Process is not completed. Process is interrupted while" +
                    " running the protoc executor.", e);
        }
        if (process.exitValue() != 0) {
            try (BufferedReader bufferedReader = new BufferedReader(new
                    InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8))) {
                String err;
                StringBuilder errMsg = new StringBuilder();
                while ((err = bufferedReader.readLine()) != null) {
                    errMsg.append(System.lineSeparator()).append(err);
                }
                throw new CLIRuntimeException(errMsg.toString());
            } catch (IOException e) {
                throw new CLIInternalException("Invalid command syntax.", e);
            }
        }
        logger.debug("Descriptor file is generation command : \"" + command + "\" is executed successfully.");
    }

    /**
     * Resolve proto folder path from Proto file path.
     *
     * @param protoPath Proto file path
     * @return Parent folder path of proto file.
     */
    private static String resolveProtoFolderPath(String protoPath) {
        int idx = protoPath.lastIndexOf(File.separator);
        String protoFolderPath = "";
        if (idx > 0) {
            protoFolderPath = protoPath.substring(0, idx);
        }
        return protoFolderPath;
    }

    /**
     * Generate OpenAPI object for the {@link DescriptorProtos.FieldDescriptorProto}.
     *
     * @param descriptor file descriptor of the protobuf
     * @return {@link OpenAPI} arraylist of openAPIs
     */
    private static ArrayList<OpenAPI> generateOpenAPIFromProto(DescriptorProtos.FileDescriptorProto descriptor,
                                                               String protoPath) {
        if (descriptor == null) {
            throw new CLIInternalException("descriptor is not available");
        }
        if (descriptor.getServiceCount() == 0) {
            return null;
        }
        ArrayList<OpenAPI> openAPIS = new ArrayList<>();
        descriptor.getServiceList().forEach(service -> {
            ProtoOpenAPI protoOpenAPI = new ProtoOpenAPI();
            if (StringUtils.isEmpty(descriptor.getPackage())) {
                protoOpenAPI.addOpenAPIInfo(service.getName());
            } else {
                protoOpenAPI.addOpenAPIInfo(descriptor.getPackage() + "." + service.getName());
            }

            //set endpoint configurations
            protoOpenAPI.addAPIProdEpExtension(generateEpList(service.getOptions()
                    .getExtension(ExtensionHolder.productionEndpoints), service.getName()));
            protoOpenAPI.addAPISandEpExtension(generateEpList(service.getOptions()
                    .getExtension(ExtensionHolder.sandboxEndpoints), service.getName()));
            //set API level security
            List<ExtensionHolder.Security> securityList = service.getOptions()
                    .getExtension(ExtensionHolder.security);
            //if nothing is mentioned regarding the security, None will be selected.
            if (securityList.size() == 0) {
                protoOpenAPI.disableAPISecurity();
            } else {
                //all the security items are added and the validation happens when retrieving the openAPI object
                if (securityList.contains(ExtensionHolder.Security.NONE)) {
                    protoOpenAPI.disableAPISecurity();
                }
                if (securityList.contains(ExtensionHolder.Security.BASIC)) {
                    protoOpenAPI.addAPIBasicSecurityRequirement();
                }
                if (securityList.contains(ExtensionHolder.Security.OAUTH2) ||
                        securityList.contains(ExtensionHolder.Security.JWT)) {
                    protoOpenAPI.addAPIOauth2SecurityRequirement();
                }
                if (securityList.contains(ExtensionHolder.Security.APIKEY)) {
                    protoOpenAPI.addAPIKeySecurityRequirement();
                }
            }
            //set API level throttling tier
            String throttlingTier = service.getOptions().getExtension(ExtensionHolder.throttlingTier);
            protoOpenAPI.setAPIThrottlingTier(throttlingTier);
            service.getMethodList().forEach(method -> {
                //set operation level scopes and throttling tiers
                String methodScopesString = method.getOptions().getExtension(ExtensionHolder.methodScopes);
                String methodThrottlingTier = method.getOptions()
                        .getExtension(ExtensionHolder.methodThrottlingTier);
                String[] methodScopes = null;
                if (!methodScopesString.isEmpty()) {
                    methodScopes = methodScopesString.split(",");
                }
                protoOpenAPI.addOpenAPIPath(method.getName(), methodScopes, methodThrottlingTier);
            });
            openAPIS.add(protoOpenAPI.getOpenAPI(service.getName()));
        });
        return openAPIS;
    }

    /**
     * Generate OpenAPI from protobuf.
     *
     * @param protoPath      protobuf file path
     * @param descriptorPath descriptor file path
     * @return {@link OpenAPI} list of OpenAPIs
     */
    public ArrayList<OpenAPI> generateOpenAPI(String protoPath, String descriptorPath) {
        DescriptorProtos.FileDescriptorProto descriptor =
                generateRootFileDescriptor(protoPath, descriptorPath);
        return generateOpenAPIFromProto(descriptor, protoPath);
    }

    /**
     * Convert protobuf endpoint configuration ({@link ExtensionHolder.Endpoints}) to the openAPI based endpoint
     * configuration ({@link EndpointListRouteDTO}).
     *
     * @param protoEps {@link ExtensionHolder.Endpoints} object.
     * @return {@link EndpointListRouteDTO} object.
     */
    private static EndpointListRouteDTO generateEpList(ExtensionHolder.Endpoints protoEps, String service) {
        EndpointListRouteDTO epList = new EndpointListRouteDTO();

        protoEps.getUrlList().forEach(epList::addEndpoint);
        try {
            epList.validateEndpoints();
        } catch (CLICompileTimeException e) {
            //todo: etcd setup needs to be tested for the protobuf scenario
            throw new CLIRuntimeException("The provided endpoint string for the gRPC \"" + service +
                    "\" is invalid.\n\t-" + e.getTerminalMsg(), e);
        }
        if (epList.getEndpoints() == null) {
            return null;
        }
        if (epList.getEndpoints().size() > 1) {
            throw new CLIRuntimeException("Multiple endpoints are not supported .service :" + service + ".");
        }
        return epList;
    }
}