/*
 * 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.maven.plugin.prefix.internal;

import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;

import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.maven.artifact.repository.metadata.Metadata;
import org.apache.maven.artifact.repository.metadata.io.MetadataReader;
import org.apache.maven.model.Build;
import org.apache.maven.model.Model;
import org.apache.maven.model.Plugin;
import org.apache.maven.model.PluginManagement;
import org.apache.maven.plugin.BuildPluginManager;
import org.apache.maven.plugin.descriptor.PluginDescriptor;
import org.apache.maven.plugin.prefix.NoPluginFoundForPrefixException;
import org.apache.maven.plugin.prefix.PluginPrefixRequest;
import org.apache.maven.plugin.prefix.PluginPrefixResolver;
import org.apache.maven.plugin.prefix.PluginPrefixResult;
import org.eclipse.aether.DefaultRepositorySystemSession;
import org.eclipse.aether.RepositoryEvent;
import org.eclipse.aether.RepositoryEvent.EventType;
import org.eclipse.aether.RepositoryListener;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.RequestTrace;
import org.eclipse.aether.metadata.DefaultMetadata;
import org.eclipse.aether.repository.ArtifactRepository;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.repository.RepositoryPolicy;
import org.eclipse.aether.resolution.MetadataRequest;
import org.eclipse.aether.resolution.MetadataResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Resolves a plugin prefix.
 *
 * @since 3.0
 */
@Named
@Singleton
public class DefaultPluginPrefixResolver implements PluginPrefixResolver {
    private static final String REPOSITORY_CONTEXT = org.apache.maven.api.services.RequestTrace.CONTEXT_PLUGIN;

    private final Logger logger = LoggerFactory.getLogger(getClass());
    private final BuildPluginManager pluginManager;
    private final RepositorySystem repositorySystem;
    private final MetadataReader metadataReader;

    @Inject
    public DefaultPluginPrefixResolver(
            BuildPluginManager pluginManager, RepositorySystem repositorySystem, MetadataReader metadataReader) {
        this.pluginManager = pluginManager;
        this.repositorySystem = repositorySystem;
        this.metadataReader = metadataReader;
    }

    @Override
    public PluginPrefixResult resolve(PluginPrefixRequest request) throws NoPluginFoundForPrefixException {
        logger.debug("Resolving plugin prefix {} from {}", request.getPrefix(), request.getPluginGroups());

        Model pom = request.getPom();
        Build build = pom != null ? pom.getBuild() : null;
        PluginManagement management = build != null ? build.getPluginManagement() : null;

        // map of groupId -> Set(artifactId) plugin candidates:
        // if value is null, keys are coming from settings, and no artifactId filtering is applied
        // if value is non-null: we allow only plugins that have enlisted artifactId only
        // ---
        // end game is: settings enlisted groupIds are obeying order and are "free for all" (artifactId)
        // while POM enlisted plugins coming from non-enlisted settings groupIds (ie conflict of prefixes)
        // will prevail/win.
        LinkedHashMap<String, Set<String>> candidates = Stream.of(build, management)
                .flatMap(container -> container != null ? container.getPlugins().stream() : Stream.empty())
                .filter(p -> !request.getPluginGroups().contains(p.getGroupId()))
                .collect(Collectors.groupingBy(
                        Plugin::getGroupId,
                        LinkedHashMap::new,
                        Collectors.mapping(Plugin::getArtifactId, Collectors.toSet())));
        request.getPluginGroups().forEach(g -> candidates.put(g, null));
        PluginPrefixResult result = null;
        // First, we go through all declared plugins, load them
        // one by one, and try to find a matching prefix.
        if (build != null) {
            result = resolveFromProject(
                    request,
                    build.getPlugins(),
                    management != null ? management.getPlugins() : Collections.emptyList());
        }

        // Second, we use G level metadata to discover prefix
        // This order allows user managed clashing prefixes (they can declare them in POM)
        if (result == null) {
            result = resolveFromRepository(request, candidates);
        }

        if (result == null) {
            throw new NoPluginFoundForPrefixException(
                    request.getPrefix(),
                    new ArrayList<>(candidates.keySet()),
                    request.getRepositorySession().getLocalRepository(),
                    request.getRepositories());
        } else {
            logger.debug(
                    "Resolved plugin prefix {} to {}:{} from repository {}",
                    request.getPrefix(),
                    result.getGroupId(),
                    result.getArtifactId(),
                    (result.getRepository() != null ? result.getRepository().getId() : "null"));
        }

        return result;
    }

    private PluginPrefixResult resolveFromProject(
            PluginPrefixRequest request, List<Plugin> plugins, List<Plugin> pluginMgmt) {
        if (plugins.isEmpty() && pluginMgmt.isEmpty()) {
            return null;
        }
        PluginPrefixResult result = null;
        Set<Plugin> candidates = new LinkedHashSet<>();
        Stream.concat(plugins.stream(), pluginMgmt.stream())
                .filter(p -> p.getArtifactId().contains(request.getPrefix()))
                .forEach(candidates::add);
        if (!candidates.isEmpty()) {
            result = doResolveFromProject(request, candidates);
        }
        if (result == null) {
            Set<Plugin> remainder = new LinkedHashSet<>(plugins);
            remainder.removeAll(candidates);
            result = doResolveFromProject(request, remainder);
        }
        return result;
    }

    private PluginPrefixResult doResolveFromProject(PluginPrefixRequest request, Collection<Plugin> plugins) {
        for (Plugin plugin : plugins) {
            try {
                PluginDescriptor pluginDescriptor =
                        pluginManager.loadPlugin(plugin, request.getRepositories(), request.getRepositorySession());

                if (request.getPrefix().equals(pluginDescriptor.getGoalPrefix())) {
                    return new DefaultPluginPrefixResult(plugin);
                }
            } catch (Exception e) {
                if (logger.isDebugEnabled()) {
                    logger.warn("Failed to retrieve plugin descriptor for {}: {}", plugin.getId(), e.getMessage(), e);
                } else {
                    logger.warn("Failed to retrieve plugin descriptor for {}: {}", plugin.getId(), e.getMessage());
                }
            }
        }

        return null;
    }

    private PluginPrefixResult resolveFromRepository(
            PluginPrefixRequest request, LinkedHashMap<String, Set<String>> candidates) {
        RequestTrace trace = RequestTrace.newChild(null, request);

        List<MetadataRequest> requests = new ArrayList<>();

        for (String pluginGroup : candidates.keySet()) {
            org.eclipse.aether.metadata.Metadata metadata =
                    new DefaultMetadata(pluginGroup, "maven-metadata.xml", DefaultMetadata.Nature.RELEASE_OR_SNAPSHOT);

            requests.add(new MetadataRequest(metadata, null, REPOSITORY_CONTEXT).setTrace(trace));

            for (RemoteRepository repository : request.getRepositories()) {
                requests.add(new MetadataRequest(metadata, repository, REPOSITORY_CONTEXT).setTrace(trace));
            }
        }

        // initial try, use locally cached metadata

        List<MetadataResult> results = repositorySystem.resolveMetadata(request.getRepositorySession(), requests);
        requests.clear();

        PluginPrefixResult result = processResults(request, trace, results, requests, candidates);

        if (result != null) {
            return result;
        }

        // second try, refetch all (possibly outdated) metadata that wasn't updated in the first attempt

        if (!request.getRepositorySession().isOffline() && !requests.isEmpty()) {
            DefaultRepositorySystemSession session = new DefaultRepositorySystemSession(request.getRepositorySession());
            session.setUpdatePolicy(RepositoryPolicy.UPDATE_POLICY_ALWAYS);

            results = repositorySystem.resolveMetadata(session, requests);

            return processResults(request, trace, results, null, candidates);
        }

        return null;
    }

    private PluginPrefixResult processResults(
            PluginPrefixRequest request,
            RequestTrace trace,
            List<MetadataResult> results,
            List<MetadataRequest> requests,
            LinkedHashMap<String, Set<String>> candidates) {
        for (MetadataResult res : results) {
            org.eclipse.aether.metadata.Metadata metadata = res.getMetadata();

            if (metadata != null) {
                ArtifactRepository repository = res.getRequest().getRepository();
                if (repository == null) {
                    repository = request.getRepositorySession().getLocalRepository();
                }

                PluginPrefixResult result =
                        resolveFromRepository(request, trace, metadata.getGroupId(), metadata, repository, candidates);

                if (result != null) {
                    return result;
                }
            }

            if (requests != null && !res.isUpdated()) {
                requests.add(res.getRequest());
            }
        }

        return null;
    }

    private PluginPrefixResult resolveFromRepository(
            PluginPrefixRequest request,
            RequestTrace trace,
            String pluginGroup,
            org.eclipse.aether.metadata.Metadata metadata,
            ArtifactRepository repository,
            LinkedHashMap<String, Set<String>> candidates) {
        if (metadata != null && metadata.getPath() != null && Files.isRegularFile(metadata.getPath())) {
            try {
                Map<String, ?> options = Collections.singletonMap(MetadataReader.IS_STRICT, Boolean.FALSE);

                Metadata pluginGroupMetadata =
                        metadataReader.read(metadata.getPath().toFile(), options);

                List<org.apache.maven.artifact.repository.metadata.Plugin> plugins = pluginGroupMetadata.getPlugins();

                if (plugins != null) {
                    for (org.apache.maven.artifact.repository.metadata.Plugin plugin : plugins) {
                        if (request.getPrefix().equals(plugin.getPrefix())
                                && (candidates.get(pluginGroup) == null
                                        || candidates.get(pluginGroup).contains(plugin.getArtifactId()))) {
                            return new DefaultPluginPrefixResult(pluginGroup, plugin.getArtifactId(), repository);
                        }
                    }
                }
            } catch (IOException e) {
                invalidMetadata(request.getRepositorySession(), trace, metadata, repository, e);
            }
        }

        return null;
    }

    private void invalidMetadata(
            RepositorySystemSession session,
            RequestTrace trace,
            org.eclipse.aether.metadata.Metadata metadata,
            ArtifactRepository repository,
            Exception exception) {
        RepositoryListener listener = session.getRepositoryListener();
        if (listener != null) {
            RepositoryEvent.Builder event = new RepositoryEvent.Builder(session, EventType.METADATA_INVALID);
            event.setTrace(trace);
            event.setMetadata(metadata);
            event.setException(exception);
            event.setRepository(repository);
            listener.metadataInvalid(event.build());
        }
    }
}
