/*
 * 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.support.langchain4j.deployment;

import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import dev.langchain4j.guardrail.InputGuardrail;
import dev.langchain4j.guardrail.JsonExtractorOutputGuardrail;
import dev.langchain4j.guardrail.OutputGuardrail;
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.V;
import io.quarkus.bootstrap.model.ApplicationModel;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.BuildSteps;
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
import io.quarkus.deployment.builditem.IndexDependencyBuildItem;
import io.quarkus.deployment.builditem.nativeimage.NativeImageProxyDefinitionBuildItem;
import io.quarkus.deployment.builditem.nativeimage.NativeImageResourcePatternsBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem;
import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem;
import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild;
import io.quarkus.maven.dependency.ResolvedDependency;
import opennlp.tools.sentdetect.SentenceDetectorFactory;
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget;
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;

@BuildSteps(onlyIf = NativeOrNativeSourcesBuild.class)
class SupportLangchain4jProcessor {
    private static final Class<?>[] AI_SERVICE_ANNOTATION_CLASSES = {
            MemoryId.class,
            SystemMessage.class,
            UserMessage.class,
            V.class
    };

    @BuildStep
    void indexDependencies(CurateOutcomeBuildItem curateOutcome, BuildProducer<IndexDependencyBuildItem> indexedDependencies) {
        ApplicationModel applicationModel = curateOutcome.getApplicationModel();
        for (ResolvedDependency dependency : applicationModel.getDependencies()) {
            if (dependency.getGroupId().equals("dev.langchain4j")) {
                indexedDependencies.produce(new IndexDependencyBuildItem(dependency.getGroupId(), dependency.getArtifactId()));
            }
        }
    }

    @BuildStep
    ServiceProviderBuildItem registerServiceProviders() {
        return ServiceProviderBuildItem.allProvidersFromClassPath("dev.langchain4j.http.client.HttpClientBuilderFactory");
    }

    @BuildStep
    void registerLangChain4jJacksonTypesForReflection(
            CombinedIndexBuildItem combinedIndex,
            BuildProducer<ReflectiveClassBuildItem> reflectiveClass) {
        IndexView index = combinedIndex.getIndex();

        // Discover all LangChain4j Jackson model types
        Set<String> langChain4jModelClasses = langChain4jTypesStream(index.getKnownClasses())
                .filter(classInfo -> classInfo.annotations().stream()
                        .anyMatch(annotationInstance -> annotationInstance.name().toString()
                                .startsWith("com.fasterxml.jackson.annotation")))
                .map(ClassInfo::name)
                .map(DotName::toString)
                .collect(Collectors.toSet());

        reflectiveClass.produce(ReflectiveClassBuildItem.builder(langChain4jModelClasses.toArray(new String[0]))
                .methods(true)
                .build());

        // Discover all LangChain4j Jackson serializer / deserializer types
        Set<String> jacksonSupportClasses = langChain4jTypesStream(index.getAllKnownSubclasses(JsonSerializer.class))
                .map(classInfo -> classInfo.name().toString())
                .collect(Collectors.toSet());

        langChain4jTypesStream(index.getAllKnownSubclasses(JsonDeserializer.class))
                .map(classInfo -> classInfo.name().toString())
                .forEach(jacksonSupportClasses::add);

        reflectiveClass.produce(ReflectiveClassBuildItem.builder(jacksonSupportClasses.toArray(new String[0])).build());

        // Misc Jackson support
        ReflectiveClassBuildItem.builder(PropertyNamingStrategies.SnakeCaseStrategy.class).build();
    }

    @BuildStep
    void registerLangChain4jAiServiceTypesForReflection(
            CombinedIndexBuildItem combinedIndex,
            BuildProducer<ReflectiveClassBuildItem> reflectiveClass,
            BuildProducer<NativeImageProxyDefinitionBuildItem> nativeImageProxy) {

        IndexView index = combinedIndex.getIndex();
        Set<String> aiServiceInterfaces = new HashSet<>();
        Set<String> aiServiceTypes = new HashSet<>();

        for (Class<?> aiServiceClass : AI_SERVICE_ANNOTATION_CLASSES) {
            for (AnnotationInstance annotationInstance : index.getAnnotations(aiServiceClass)) {
                AnnotationTarget annotationTarget = annotationInstance.target();

                if (annotationTarget.kind().equals(AnnotationTarget.Kind.CLASS)) {
                    aiServiceInterfaces.add(annotationTarget.asClass().name().toString());
                } else if (annotationTarget.kind().equals(AnnotationTarget.Kind.METHOD)) {
                    MethodInfo method = annotationTarget.asMethod();
                    aiServiceInterfaces.add(method.declaringClass().name().toString());
                    if (!method.returnType().kind().equals(Type.Kind.VOID)) {
                        aiServiceTypes.add(method.returnType().name().toString());
                    }
                } else if (annotationTarget.kind().equals(AnnotationTarget.Kind.METHOD_PARAMETER)) {
                    MethodParameterInfo methodParameter = annotationTarget.asMethodParameter();
                    aiServiceTypes.add(methodParameter.type().name().toString());

                    MethodInfo method = methodParameter.method();
                    aiServiceInterfaces.add(method.declaringClass().name().toString());
                    if (!method.returnType().kind().equals(Type.Kind.VOID)) {
                        aiServiceTypes.add(method.returnType().name().toString());
                    }
                }
            }
        }

        // Any types participating in JsonExtractorOutputGuardrail operations require reflection
        index.getAllKnownSubclasses(JsonExtractorOutputGuardrail.class)
                .stream()
                .filter(classInfo -> classInfo.superClassType() != null)
                .filter(classInfo -> classInfo.superClassType().kind().equals(Type.Kind.PARAMETERIZED_TYPE))
                .map(ClassInfo::superClassType)
                .map(Type::asParameterizedType)
                .flatMap(type -> type.arguments().stream())
                .findFirst()
                .ifPresent(typeParameter -> {
                    aiServiceTypes.add(typeParameter.name().toString());
                });

        // AI service interfaces must be registered as native image proxies
        aiServiceInterfaces
                .stream()
                .map(NativeImageProxyDefinitionBuildItem::new)
                .forEach(nativeImageProxy::produce);

        // Register any types related to the AI service for reflection
        reflectiveClass.produce(ReflectiveClassBuildItem.builder(aiServiceTypes.toArray(new String[0]))
                .fields()
                .methods()
                .build());

        // Guardrails are instantiated dynamically
        Set<String> guardrailTypes = index.getAllKnownImplementations(InputGuardrail.class)
                .stream()
                .map(classInfo -> classInfo.name().toString())
                .collect(Collectors.toSet());

        index.getAllKnownImplementations(OutputGuardrail.class)
                .stream()
                .map(classInfo -> classInfo.name().toString())
                .forEach(guardrailTypes::add);

        reflectiveClass.produce(ReflectiveClassBuildItem.builder(guardrailTypes.toArray(new String[0])).build());
    }

    @BuildStep
    void registerLangChain4jNlpTypesForReflection(BuildProducer<ReflectiveClassBuildItem> reflectiveClass) {
        reflectiveClass.produce(ReflectiveClassBuildItem.builder(SentenceDetectorFactory.class).build());
    }

    @BuildStep
    RuntimeInitializedClassBuildItem runtimeInitializedClasses() {
        return new RuntimeInitializedClassBuildItem("dev.langchain4j.internal.RetryUtils");
    }

    @BuildStep
    NativeImageResourcePatternsBuildItem nativeImageResources() {
        return NativeImageResourcePatternsBuildItem.builder()
                .includeGlob("opennlp/*.bin")
                .build();
    }

    static Stream<ClassInfo> langChain4jTypesStream(Collection<ClassInfo> classes) {
        return classes.stream()
                .filter(classInfo -> classInfo.name().toString().startsWith("dev.langchain4j"));
    }
}
