/*
 * 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.camel.quarkus.component.grpc.deployment;

import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import com.google.api.client.json.GenericJson;
import io.grpc.BindableService;
import io.grpc.stub.AbstractAsyncStub;
import io.grpc.stub.AbstractBlockingStub;
import io.grpc.stub.AbstractFutureStub;
import io.grpc.stub.AbstractStub;
import io.grpc.stub.StreamObserver;
import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.deployment.GeneratedBeanBuildItem;
import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
import io.quarkus.deployment.builditem.FeatureBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.gizmo.ClassCreator;
import io.quarkus.gizmo.FieldCreator;
import io.quarkus.gizmo.MethodCreator;
import io.quarkus.gizmo.MethodDescriptor;
import io.quarkus.gizmo.ResultHandle;
import jakarta.enterprise.context.Dependent;
import org.apache.camel.component.grpc.server.GrpcMethodHandler;
import org.apache.camel.quarkus.grpc.runtime.CamelQuarkusBindableService;
import org.apache.camel.quarkus.grpc.runtime.GrpcBuildTimeConfig;
import org.apache.camel.quarkus.grpc.runtime.QuarkusBindableServiceFactory;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.IndexView;
import org.jboss.jandex.MethodInfo;
import org.jboss.jandex.MethodParameterInfo;
import org.jboss.jandex.Type;
import org.jboss.logging.Logger;

class GrpcProcessor {
    private static final Logger LOG = Logger.getLogger(GrpcProcessor.class);
    private static final DotName BINDABLE_SERVICE_DOT_NAME = DotName.createSimple(BindableService.class.getName());
    private static final DotName[] STUB_CLASS_DOT_NAMES = new DotName[] {
            DotName.createSimple(AbstractAsyncStub.class.getName()),
            DotName.createSimple(AbstractBlockingStub.class.getName()),
            DotName.createSimple(AbstractFutureStub.class.getName())
    };
    private static final String FEATURE = "camel-grpc";

    @BuildStep
    FeatureBuildItem feature() {
        return new FeatureBuildItem(FEATURE);
    }

    @BuildStep
    CamelGrpcServiceExcludesBuildItem camelGrpcServiceExcludes(GrpcBuildTimeConfig config) {
        return new CamelGrpcServiceExcludesBuildItem(config.serviceExcludes().orElse(Collections.emptySet()));
    }

    @BuildStep
    void registerForReflection(
            BuildProducer<ReflectiveClassBuildItem> reflectiveClass,
            CombinedIndexBuildItem combinedIndexBuildItem,
            CamelGrpcServiceExcludesBuildItem camelGrpcServiceExcludes) {

        IndexView index = combinedIndexBuildItem.getIndex();
        for (DotName dotName : STUB_CLASS_DOT_NAMES) {
            index.getAllKnownSubclasses(dotName)
                    .stream()
                    .filter(camelGrpcServiceExcludes.serviceExcludesFilter())
                    .peek(classInfo -> logDebugMessage("Discovered gRPC stub class %s", classInfo.name().toString()))
                    .map(classInfo -> ReflectiveClassBuildItem.builder(classInfo.name().toString()).methods()
                            .build())
                    .forEach(reflectiveClass::produce);
        }
        reflectiveClass
                .produce(ReflectiveClassBuildItem.builder(AbstractStub.class.getName()).methods().build());
        reflectiveClass.produce(ReflectiveClassBuildItem.builder(GenericJson.class.getName()).build());
    }

    @BuildStep
    void quarkusBindableServiceFactoryBean(BuildProducer<AdditionalBeanBuildItem> additionalBeans) {
        additionalBeans.produce(AdditionalBeanBuildItem.unremovableOf(QuarkusBindableServiceFactory.class));
    }

    @BuildStep
    void createBindableServiceBeans(
            BuildProducer<GeneratedBeanBuildItem> generatedBean,
            BuildProducer<ReflectiveClassBuildItem> reflectiveClass,
            CombinedIndexBuildItem combinedIndexBuildItem,
            CamelGrpcServiceExcludesBuildItem camelGrpcServiceExcludes) {

        IndexView index = combinedIndexBuildItem.getIndex();
        Collection<ClassInfo> bindableServiceImpls = index.getAllKnownImplementations(BINDABLE_SERVICE_DOT_NAME)
                .stream()
                .filter(camelGrpcServiceExcludes.serviceExcludesFilter())
                .collect(Collectors.toSet());

        // Generate implementation classes from any abstract gRPC BindableService implementations included in the application archive
        // Override the various sync and async methods so that requests can be intercepted and delegated to Camel routing
        // This mimics similar logic in DefaultBindableServiceFactory that uses Javassist ProxyFactory & MethodHandler
        for (ClassInfo service : bindableServiceImpls) {
            String superClassName = service.name().toString();
            String generatedClassName = superClassName + "QuarkusMethodHandler";

            if (!Modifier.isAbstract(service.flags())) {
                logDebugMessage("Ignoring BindableService %s as it is not an interface or abstract class", superClassName);
                continue;
            }

            if (service.name().withoutPackagePrefix().startsWith("Mutiny")) {
                /* The generate-code goal of quarkus-maven-plugin generates also Mutiny service that we do not use
                 * Not skipping it here results in randomly registering the Mutiny one or the right one.
                 * In case the Mutiny service one is registered, the client throws something like
                 * io.grpc.StatusRuntimeException: UNIMPLEMENTED */
                logDebugMessage("Ignoring BindableService %s as it a Mutiny service", superClassName);
                continue;
            }

            Optional<String> asyncServiceInterface = service.interfaceNames()
                    .stream()
                    .map(DotName::toString)
                    .filter(className -> className.endsWith("AsyncService"))
                    .findFirst();
            if (asyncServiceInterface.isEmpty()) {
                logDebugMessage("Ignoring BindableService %s as it does not implement AsyncService", superClassName);
                continue;
            }

            // Register the service classes for reflection
            reflectiveClass
                    .produce(ReflectiveClassBuildItem.builder(superClassName).methods().build());
            reflectiveClass.produce(
                    ReflectiveClassBuildItem.builder(service.enclosingClass().toString()).methods().build());
            reflectiveClass.produce(ReflectiveClassBuildItem.builder(generatedClassName).methods().build());

            logDebugMessage("Generating CamelQuarkusBindableService %s extending %s", generatedClassName, superClassName);

            try (ClassCreator classCreator = ClassCreator.builder()
                    .classOutput(new GeneratedBeanGizmoAdaptor(generatedBean))
                    .className(generatedClassName)
                    .superClass(superClassName)
                    .interfaces(CamelQuarkusBindableService.class)
                    .build()) {

                classCreator.addAnnotation(Dependent.class);

                FieldCreator serverMethodHandler = classCreator
                        .getFieldCreator("methodHandler", GrpcMethodHandler.class.getName())
                        .setModifiers(Modifier.PRIVATE);

                // Create constructor
                try (MethodCreator initMethod = classCreator.getMethodCreator("<init>", void.class)) {
                    initMethod.setModifiers(Modifier.PUBLIC);
                    initMethod.invokeSpecialMethod(MethodDescriptor.ofMethod(superClassName, "<init>", void.class),
                            initMethod.getThis());
                    initMethod.returnValue(null);
                }

                // Create setMethodHandler override
                try (MethodCreator setMethodHandlerMethod = classCreator.getMethodCreator("setMethodHandler", void.class,
                        GrpcMethodHandler.class)) {
                    setMethodHandlerMethod.setModifiers(Modifier.PUBLIC);

                    ResultHandle self = setMethodHandlerMethod.getThis();
                    ResultHandle methodHandlerInstance = setMethodHandlerMethod.getMethodParam(0);

                    setMethodHandlerMethod.writeInstanceField(serverMethodHandler.getFieldDescriptor(), self,
                            methodHandlerInstance);
                    setMethodHandlerMethod.returnValue(null);
                }

                // Override service methods that the gRPC component is interested in
                // E.g methods with one or two parameters where one is of type StreamObserver
                ClassInfo asyncServiceClassInfo = index.getClassByName(asyncServiceInterface.get());
                List<MethodInfo> methods = asyncServiceClassInfo.methods();
                for (MethodInfo method : methods) {
                    if (isCandidateServiceMethod(method)) {
                        String[] params = method.parameters()
                                .stream()
                                .map(MethodParameterInfo::type)
                                .map(Type::name)
                                .map(DotName::toString)
                                .toArray(String[]::new);

                        ClassInfo classInfo = index
                                .getClassByName(DotName.createSimple(GrpcMethodHandler.class.getName()));

                        String returnType = method.returnType().name().toString();
                        try (MethodCreator methodCreator = classCreator.getMethodCreator(method.name(), returnType, params)) {
                            logDebugMessage("Creating service implementation for method %s in %s", method.name(),
                                    generatedClassName);

                            method.exceptions()
                                    .stream()
                                    .map(type -> type.name().toString())
                                    .forEach(methodCreator::addException);

                            if (method.parameters().size() == 1) {
                                ResultHandle returnValue = generateGrpcDelegateMethod(classInfo, serverMethodHandler,
                                        methodCreator,
                                        method, "handleForConsumerStrategy");
                                methodCreator.returnValue(returnValue);
                            } else if (method.parameters().size() == 2) {
                                generateGrpcDelegateMethod(classInfo, serverMethodHandler, methodCreator, method,
                                        "handle");
                                methodCreator.returnValue(null);
                            }
                        }
                    }
                }
            }
        }
    }

    private boolean isCandidateServiceMethod(MethodInfo method) {
        List<MethodParameterInfo> parameters = method.parameters();
        if (parameters.size() == 1) {
            return parameters.get(0).type().name().toString().equals(StreamObserver.class.getName());
        } else if (parameters.size() == 2) {
            return parameters.get(1).type().name().toString().equals(StreamObserver.class.getName());
        }
        return false;
    }

    private ResultHandle generateGrpcDelegateMethod(ClassInfo classInfo, FieldCreator fieldCreator, MethodCreator methodCreator,
            MethodInfo sourceMethod, String targetMethod) {

        MethodInfo method = classInfo.methods()
                .stream()
                .filter(methodInfo -> methodInfo.name().equals(targetMethod))
                .findFirst()
                .orElseThrow(() -> new RuntimeException(
                        "Unable to find target method " + targetMethod + " on GrpcServerMethodHandler"));

        ResultHandle methodNameParam = methodCreator.load(sourceMethod.name());
        ResultHandle[] methodParams;
        if (sourceMethod.parameters().size() == 1) {
            methodParams = new ResultHandle[] { methodCreator.getMethodParam(0), methodNameParam };
        } else {
            methodParams = new ResultHandle[] { methodCreator.getMethodParam(0), methodCreator.getMethodParam(1),
                    methodNameParam };
        }

        ResultHandle resultHandle = methodCreator.readInstanceField(fieldCreator.getFieldDescriptor(), methodCreator.getThis());
        return methodCreator.invokeVirtualMethod(method, resultHandle, methodParams);
    }

    private void logDebugMessage(String message, Object... params) {
        if (LOG.isDebugEnabled()) {
            LOG.debugf(message, params);
        }
    }
}
