package com.github.thinkerou.karate.service;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import com.github.thinkerou.karate.constants.DescriptorFile;
import com.github.thinkerou.karate.domain.ProtoName;
import com.github.thinkerou.karate.protobuf.ProtoFullName;
import com.github.thinkerou.karate.protobuf.ServiceResolver;
import com.github.thinkerou.karate.utils.FileHelper;
import com.github.thinkerou.karate.utils.RedisHelper;
import com.google.gson.Gson;
import com.google.protobuf.DescriptorProtos;
import com.google.protobuf.Descriptors;

/**
 * GrpcList
 *
 * Utility to list the services, methods and message definitions for the known grpc end-points.
 *
 * @author thinkerou
 */
public final class GrpcList {

    public static GrpcList create() {
        return new GrpcList();
    }

    public GrpcList() {
    }

    /**
     * Support format: packageName.serviceName/methodName
     */
    public String invoke(String name, Boolean withMessage) {
        return new Gson().toJson(execute(name, withMessage, null));
    }

    public String invoke(String service, String method, Boolean withMessage) {
        return new Gson().toJson(execute(service, method, withMessage, false, null));
    }

    public String invokeByRedis(String name, Boolean withMessage, RedisHelper redisHelper) {
        return new Gson().toJson(execute(name, withMessage, redisHelper));
    }

    public String invokeByRedis(String service, String method, Boolean withMessage, RedisHelper redisHelper) {
        return new Gson().toJson(execute(service, method, withMessage, false, redisHelper));
    }
    private List<Map<String, Object>> execute(String name, Boolean withMessage, RedisHelper redisHelper) {
        ProtoName protoName = ProtoFullName.parse(name);
        return execute(protoName.getServiceName(), protoName.getMethodName(), withMessage, false, redisHelper);
    }

    /**
     * List the grpc services filtered by service name (contains) or method name (contains).
     *
     * Mainly goal: return value are used web page.
     */
    private List<Map<String, Object>> execute(
            String serviceFilter,
            String methodFilter,
            Boolean withMessage,
            Boolean saveOutput,
            RedisHelper redisHelper) {
        byte[] data;
        if (redisHelper != null) {
            data = redisHelper.getDescriptorSets();
        } else {
            String path = System.getProperty("user.home") + DescriptorFile.PROTO_PATH.getText();
            Path descriptorPath = Paths.get(path + DescriptorFile.PROTO_FILE.getText());
            FileHelper.validatePath(Optional.ofNullable(descriptorPath));
            try {
                data = Files.readAllBytes(descriptorPath);
            } catch (IOException e) {
                throw new IllegalArgumentException("Read descriptor fail: " + descriptorPath.toString());
            }
        }

        // Fetch the appropriate file descriptors for the service.
        DescriptorProtos.FileDescriptorSet fileDescriptorSet;
        try {
            fileDescriptorSet = DescriptorProtos.FileDescriptorSet.parseFrom(data);
        } catch (IOException e) {
            throw new IllegalArgumentException("Descriptor file parse fail: " + e.getMessage());
        }

        ServiceResolver serviceResolver = ServiceResolver.fromFileDescriptorSet(fileDescriptorSet);

        Iterable<Descriptors.ServiceDescriptor> serviceDescriptorIterable = serviceResolver.listServices();

        List<Map<String, Object>> output = new ArrayList<>();
        serviceDescriptorIterable.forEach(descriptor -> {
            if (serviceFilter.isEmpty()
                    || descriptor.getFullName().toLowerCase().contains(serviceFilter.toLowerCase())) {
                Map<String, Object> result = new HashMap<>();
                listMethods(result, descriptor, methodFilter, withMessage, saveOutput);

                if (!result.isEmpty()) {
                    output.add(result);
                }
            }
        });

        return output;
    }

    /**
     * List the methods on the service (the mothodFilter will be applied if non empty.
     */
    private static void listMethods(
            Map<String, Object> output,
            Descriptors.ServiceDescriptor descriptor,
            String methodFilter,
            Boolean withMessage,
            Boolean saveOutputInfo) {
        List<Descriptors.MethodDescriptor> methodDescriptors = descriptor.getMethods();

        methodDescriptors.forEach(method -> {
            if (methodFilter.isEmpty() || method.getName().contains(methodFilter)) {
                String key = descriptor.getFullName() + "/" + method.getName();

                Map<String, Object> res = new HashMap<>();
                res.put("file", descriptor.getFile().getName());

                // If requested, add the message definition
                if (withMessage) {
                    Map<String, Object> o = new HashMap<>();
                    o.put(method.getInputType().getName(), renderDescriptor(method.getInputType()));
                    res.put("input", o);
                    if (saveOutputInfo) {
                        Map<String, Object> oo = new HashMap<>();
                        oo.put(method.getOutputType().getName(), renderDescriptor(method.getOutputType()));
                        res.put("output", oo);
                    }
                }
                output.put(key, res);
            }
        });
    }

    /**
     * Create a human readable string to help the user build a message to send to an end-point.
     */
    private static Map<String, Object> renderDescriptor(Descriptors.Descriptor descriptor) {
        Map<String, Object> result = new HashMap<>();

        if (descriptor.getFields().size() == 0) {
            result.put("EMPTY", "");
            return result;
        }

        for (Descriptors.FieldDescriptor field : descriptor.getFields()) {
            String isOpt = field.isOptional() ? "OPTIONAL" : "REQUIRED";
            String isRep = field.isRepeated() ? "REPEATED" : "SINGLE";
            String fieldPrefix = field.getJsonName() + "." + isOpt + "." + isRep;
            result.put(fieldPrefix, renderFieldDescriptor(field));
        }

        return result;
    }

    /**
     * Create a readable string from the field to help the user build a message.
     */
    private static Object renderFieldDescriptor(Descriptors.FieldDescriptor descriptor) {
        switch (descriptor.getJavaType()) {
            case MESSAGE:
                return renderDescriptor(descriptor.getMessageType());
            case ENUM:
                return descriptor.getEnumType().getValues();
            default:
                return descriptor.getJavaType();
        }
    }

}