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

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import io.quarkus.bootstrap.model.ApplicationModel;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.builditem.FeatureBuildItem;
import io.quarkus.deployment.builditem.nativeimage.NativeImageResourcePatternsBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem;
import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild;
import io.quarkus.maven.dependency.ResolvedDependency;
import io.quarkus.paths.PathFilter;
import io.quarkus.paths.PathVisitor;
import org.apache.camel.quarkus.component.kamelet.KameletConfiguration;
import org.apache.camel.util.FileUtil;
import org.apache.camel.util.ObjectHelper;
import org.apache.camel.util.StringHelper;
import org.eclipse.microprofile.config.ConfigProvider;
import org.jboss.logging.Logger;

class KameletProcessor {
    private static final Logger LOGGER = Logger.getLogger(KameletProcessor.class);
    private static final String CLASS_PREFIX = "#class:";
    private static final String CLASSPATH_PREFIX = "classpath";
    private static final String FILE_PREFIX = "file";
    private static final String KAMELET_FILE_EXTENSION = ".kamelet.yaml";
    private static final String FEATURE = "camel-kamelet";

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

    @BuildStep(onlyIf = NativeOrNativeSourcesBuild.class)
    void kameletNativeModeSupport(
            CurateOutcomeBuildItem curateOutcome,
            BuildProducer<NativeImageResourcePatternsBuildItem> nativeImagePatterns,
            BuildProducer<ReflectiveClassBuildItem> reflectiveClass,
            KameletConfiguration kameletConfig) {

        // The locations where kamelets are stored
        List<String> kameletLocations = ConfigProvider.getConfig()
                .getOptionalValues("camel.component.kamelet.location", String.class)
                .orElse(List.of("classpath:kamelets"));

        Set<String> identifiers = kameletConfig.identifiers()
                .stream()
                .map(String::trim)
                .map(identifier -> identifier.replace(KAMELET_FILE_EXTENSION, ""))
                .map(identifier -> identifier + KAMELET_FILE_EXTENSION)
                .collect(Collectors.toUnmodifiableSet());

        List<String> kameletResources = new ArrayList<>();
        Set<String> kameletClasspathPatterns = new HashSet<>();
        for (String kameletLocation : kameletLocations) {
            String scheme = StringHelper.before(kameletLocation, ":");
            String location = StringHelper.after(kameletLocation, ":", kameletLocation);
            if (ObjectHelper.isEmpty(scheme) || scheme.equals(CLASSPATH_PREFIX)) {
                identifiers.forEach(identifier -> kameletClasspathPatterns
                        .add(FileUtil.stripLeadingSeparator(location) + "/" + identifier));
            } else if (scheme.equals(FILE_PREFIX)) {
                boolean isDefaultIdentifier = identifiers.size() == 1 && identifiers.contains("*." + KAMELET_FILE_EXTENSION);

                Path kameletsDir = Paths.get(location);
                if (Files.isDirectory(kameletsDir)) {
                    try {
                        Files.walkFileTree(kameletsDir, new SimpleFileVisitor<>() {
                            @Override
                            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
                                if (Files.isReadable(file)) {
                                    String fileName = file.getFileName().toString();
                                    if ((isDefaultIdentifier && fileName.endsWith(KAMELET_FILE_EXTENSION))
                                            || (identifiers.contains(fileName))) {
                                        kameletResources.add(FILE_PREFIX + ":" + file.toAbsolutePath());
                                    }
                                }
                                return FileVisitResult.CONTINUE;
                            }

                            @Override
                            public FileVisitResult visitFileFailed(Path file, IOException e) {
                                return FileVisitResult.CONTINUE;
                            }
                        });
                    } catch (IOException e) {
                        LOGGER.debugf(e, "Failed to walk kamelet directory %s", location);
                    }
                }
            }
        }

        if (!kameletClasspathPatterns.isEmpty()) {
            nativeImagePatterns.produce(NativeImageResourcePatternsBuildItem.builder()
                    .includeGlobs(kameletClasspathPatterns)
                    .build());

            PathFilter pathFilter = PathFilter.forIncludes(kameletClasspathPatterns);
            PathVisitor pathVisitor = visit -> kameletResources.add(CLASSPATH_PREFIX + ":" + sanitizePath(visit.getPath()));

            // Discover Kamelets in the application artifact
            ApplicationModel applicationModel = curateOutcome.getApplicationModel();
            ResolvedDependency appArtifact = applicationModel.getAppArtifact();
            appArtifact.getContentTree(pathFilter).walk(pathVisitor);

            // Discover Kamelets in other runtime dependencies
            for (ResolvedDependency dependency : applicationModel.getRuntimeDependencies()) {
                dependency.getContentTree(pathFilter).walk(pathVisitor);
            }
        }

        // Automatically register kamelet beans for reflection
        if (!kameletResources.isEmpty()) {
            Set<String> kameletBeanClasses = resolveKameletBeanClasses(kameletResources);
            if (!kameletBeanClasses.isEmpty()) {
                reflectiveClass.produce(ReflectiveClassBuildItem.builder(kameletBeanClasses.toArray(new String[0]))
                        .fields()
                        .methods()
                        .build());
            }
        }
    }

    private Set<String> resolveKameletBeanClasses(List<String> kameletResources) {
        Set<String> kameletBeanClasses = new HashSet<>();
        for (String kameletResource : kameletResources) {
            LOGGER.debugf("Processing kamelet resource %s", kameletResource);

            try (InputStream stream = getKameletAsStream(kameletResource)) {
                if (stream == null) {
                    continue;
                }

                // Parse out any #class: references from the YAML. Avoids having to instantiate a CamelContext or YAML parser
                try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) {
                    String line;

                    while ((line = reader.readLine()) != null) {
                        if (line.startsWith("#")) {
                            continue;
                        }

                        String beanClass = StringHelper.after(line, CLASS_PREFIX);
                        if (ObjectHelper.isNotEmpty(beanClass)) {
                            beanClass = StringHelper.removeQuotes(beanClass);

                            // Handle #class declarations that include constructor args
                            if (beanClass.contains("(")) {
                                beanClass = StringHelper.before(beanClass, "(");
                            }

                            // Ignore #class declarations with property placeholders
                            if (beanClass.contains("{{")) {
                                continue;
                            }

                            LOGGER.debugf("Discovered kamelet bean class %s", beanClass);

                            kameletBeanClasses.add(beanClass);
                        }
                    }
                }
            } catch (IOException e) {
                LOGGER.debugf(e, "Failed processing kamelet resource %s", kameletResource);
            }
        }
        return kameletBeanClasses;
    }

    private InputStream getKameletAsStream(String kameletResource) throws IOException {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        String scheme = StringHelper.before(kameletResource, ":");
        String location = StringHelper.after(kameletResource, ":", kameletResource);
        if (ObjectHelper.isEmpty(scheme) || scheme.equals(CLASSPATH_PREFIX)) {
            return classLoader.getResourceAsStream(location);
        } else if (scheme.equals(FILE_PREFIX)) {
            return Files.newInputStream(Paths.get(location));
        }
        return null;
    }

    private String sanitizePath(Path path) {
        String normalizedPath = FileUtil.normalizePath(path.toString());
        return FileUtil.stripLeadingSeparator(normalizedPath);
    }
}
