/*
 * 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.shared.release.phase;

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

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;

import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.ArtifactUtils;
import org.apache.maven.project.MavenProject;
import org.apache.maven.shared.release.ReleaseExecutionException;
import org.apache.maven.shared.release.ReleaseFailureException;
import org.apache.maven.shared.release.ReleaseResult;
import org.apache.maven.shared.release.config.ReleaseDescriptor;
import org.apache.maven.shared.release.env.ReleaseEnvironment;
import org.apache.maven.shared.release.versions.DefaultVersionInfo;
import org.apache.maven.shared.release.versions.VersionInfo;
import org.apache.maven.shared.release.versions.VersionParseException;
import org.codehaus.plexus.components.interactivity.Prompter;
import org.codehaus.plexus.components.interactivity.PrompterException;

import static java.util.Objects.requireNonNull;

/**
 * Check the dependencies of all projects being released to see if there are any unreleased snapshots.
 *
 * @author <a href="mailto:brett@apache.org">Brett Porter</a>
 */
// TODO plugins with no version will be resolved to RELEASE which is not a snapshot, but remains unresolved to this
// point. This is a potential hole in the check, and should be revisited after the release pom writing is done and
// resolving versions to verify whether it is.
// TODO plugins injected by the lifecycle are not tested here. They will be injected with a RELEASE version so are
// covered under the above point.
@Singleton
@Named("check-dependency-snapshots")
public class CheckDependencySnapshotsPhase extends AbstractReleasePhase {
    public static final String RESOLVE_SNAPSHOT_MESSAGE = "There are still some remaining snapshot dependencies.\n";

    public static final String RESOLVE_SNAPSHOT_PROMPT = "Do you want to resolve them now?";

    public static final String RESOLVE_SNAPSHOT_TYPE_MESSAGE = "Dependency type to resolve,";

    public static final String RESOLVE_SNAPSHOT_TYPE_PROMPT =
            "specify the selection number ( 0:All 1:Project Dependencies 2:Plugins 3:Reports 4:Extensions ):";

    /**
     * Component used to prompt for input.
     */
    private final AtomicReference<Prompter> prompter;

    // Be aware of the difference between usedSnapshots and specifiedSnapshots:
    // UsedSnapshots end up on the classpath.
    // SpecifiedSnapshots are defined anywhere in the pom.
    // We'll probably need to introduce specifiedSnapshots as well.
    // @TODO MRELEASE-378: verify custom dependencies in plugins. Be aware of deprecated/removed Components in M3, such
    // as PluginCollector
    // @TODO MRELEASE-763: verify all dependencies in inactive profiles

    // Don't prompt for every project in reactor, remember state of questions
    private String resolveSnapshot;

    private String resolveSnapshotType;

    @Inject
    public CheckDependencySnapshotsPhase(Prompter prompter) {
        this.prompter = new AtomicReference<>(requireNonNull(prompter));
    }

    /**
     * For easier testing only!
     */
    public void setPrompter(Prompter prompter) {
        this.prompter.set(prompter);
    }

    @Override
    public ReleaseResult execute(
            ReleaseDescriptor releaseDescriptor,
            ReleaseEnvironment releaseEnvironment,
            List<MavenProject> reactorProjects)
            throws ReleaseExecutionException, ReleaseFailureException {
        ReleaseResult result = new ReleaseResult();

        if (!releaseDescriptor.isAllowTimestampedSnapshots()) {
            logInfo(result, "Checking dependencies and plugins for snapshots ...");

            for (MavenProject project : reactorProjects) {
                checkProject(project, releaseDescriptor);
            }
        } else {
            logInfo(result, "Ignoring SNAPSHOT dependencies and plugins ...");
        }
        result.setResultCode(ReleaseResult.SUCCESS);

        return result;
    }

    private void checkProject(MavenProject project, ReleaseDescriptor releaseDescriptor)
            throws ReleaseFailureException, ReleaseExecutionException {
        Map<String, Artifact> artifactMap = ArtifactUtils.artifactMapByVersionlessId(project.getArtifacts());

        Set<Artifact> usedSnapshotDependencies = new HashSet<>();

        if (project.getParentArtifact() != null) {
            if (checkArtifact(project.getParentArtifact(), artifactMap, releaseDescriptor)) {
                usedSnapshotDependencies.add(project.getParentArtifact());
            }
        }

        Set<Artifact> dependencyArtifacts = project.getDependencyArtifacts();
        usedSnapshotDependencies.addAll(checkDependencies(releaseDescriptor, artifactMap, dependencyArtifacts));

        // @todo check dependencyManagement

        Set<Artifact> pluginArtifacts = project.getPluginArtifacts();
        Set<Artifact> usedSnapshotPlugins = checkPlugins(releaseDescriptor, artifactMap, pluginArtifacts);

        // @todo check pluginManagement

        Set<Artifact> reportArtifacts = project.getReportArtifacts();
        Set<Artifact> usedSnapshotReports = checkReports(releaseDescriptor, artifactMap, reportArtifacts);

        Set<Artifact> extensionArtifacts = project.getExtensionArtifacts();
        Set<Artifact> usedSnapshotExtensions = checkExtensions(releaseDescriptor, artifactMap, extensionArtifacts);

        // @todo check profiles

        if (!usedSnapshotDependencies.isEmpty()
                || !usedSnapshotReports.isEmpty()
                || !usedSnapshotExtensions.isEmpty()
                || !usedSnapshotPlugins.isEmpty()) {
            if (releaseDescriptor.isInteractive() || null != releaseDescriptor.getAutoResolveSnapshots()) {
                resolveSnapshots(
                        usedSnapshotDependencies,
                        usedSnapshotReports,
                        usedSnapshotExtensions,
                        usedSnapshotPlugins,
                        releaseDescriptor);
            }

            if (!usedSnapshotDependencies.isEmpty()
                    || !usedSnapshotReports.isEmpty()
                    || !usedSnapshotExtensions.isEmpty()
                    || !usedSnapshotPlugins.isEmpty()) {
                StringBuilder message = new StringBuilder();

                printSnapshotDependencies(usedSnapshotDependencies, message);
                printSnapshotDependencies(usedSnapshotReports, message);
                printSnapshotDependencies(usedSnapshotExtensions, message);
                printSnapshotDependencies(usedSnapshotPlugins, message);
                message.append("in project '" + project.getName() + "' (" + project.getId() + ")");

                throw new ReleaseFailureException(
                        "Can't release project due to non released dependencies :\n" + message);
            }
        }
    }

    private Set<Artifact> checkPlugins(
            ReleaseDescriptor releaseDescriptor, Map<String, Artifact> artifactMap, Set<Artifact> pluginArtifacts)
            throws ReleaseExecutionException {
        Set<Artifact> usedSnapshotPlugins = new HashSet<>();
        for (Artifact artifact : pluginArtifacts) {
            if (checkArtifact(artifact, artifactMap, releaseDescriptor)) {
                boolean addToFailures;

                if ("org.apache.maven.plugins".equals(artifact.getGroupId())
                        && "maven-release-plugin".equals(artifact.getArtifactId())) {
                    // It's a snapshot of the release plugin. Maybe just testing - ask
                    // By default, we fail as for any other plugin
                    if (releaseDescriptor.isSnapshotReleasePluginAllowed()) {
                        addToFailures = false;
                    } else if (releaseDescriptor.isInteractive()) {
                        try {
                            String result;
                            if (!releaseDescriptor.isSnapshotReleasePluginAllowed()) {
                                prompter.get()
                                        .showMessage("This project relies on a SNAPSHOT of the release plugin. "
                                                + "This may be necessary during testing.\n");
                                result = prompter.get()
                                        .prompt(
                                                "Do you want to continue with the release?",
                                                Arrays.asList("yes", "no"),
                                                "no");
                            } else {
                                result = "yes";
                            }

                            addToFailures = !result.toLowerCase(Locale.ENGLISH).startsWith("y");
                        } catch (PrompterException e) {
                            throw new ReleaseExecutionException(e.getMessage(), e);
                        }
                    } else {
                        addToFailures = true;
                    }
                } else {
                    addToFailures = true;
                }

                if (addToFailures) {
                    usedSnapshotPlugins.add(artifact);
                }
            }
        }
        return usedSnapshotPlugins;
    }

    private Set<Artifact> checkDependencies(
            ReleaseDescriptor releaseDescriptor, Map<String, Artifact> artifactMap, Set<Artifact> dependencyArtifacts) {
        Set<Artifact> usedSnapshotDependencies = new HashSet<>();
        for (Artifact artifact : dependencyArtifacts) {
            if (checkArtifact(artifact, artifactMap, releaseDescriptor)) {
                usedSnapshotDependencies.add(getArtifactFromMap(artifact, artifactMap));
            }
        }
        return usedSnapshotDependencies;
    }

    private Set<Artifact> checkReports(
            ReleaseDescriptor releaseDescriptor, Map<String, Artifact> artifactMap, Set<Artifact> reportArtifacts) {
        Set<Artifact> usedSnapshotReports = new HashSet<>();
        for (Artifact artifact : reportArtifacts) {
            if (checkArtifact(artifact, artifactMap, releaseDescriptor)) {
                // snapshotDependencies.add( artifact );
                usedSnapshotReports.add(artifact);
            }
        }
        return usedSnapshotReports;
    }

    private Set<Artifact> checkExtensions(
            ReleaseDescriptor releaseDescriptor, Map<String, Artifact> artifactMap, Set<Artifact> extensionArtifacts) {
        Set<Artifact> usedSnapshotExtensions = new HashSet<>();
        for (Artifact artifact : extensionArtifacts) {
            if (checkArtifact(artifact, artifactMap, releaseDescriptor)) {
                usedSnapshotExtensions.add(artifact);
            }
        }
        return usedSnapshotExtensions;
    }

    private static boolean checkArtifact(
            Artifact artifact, Map<String, Artifact> artifactMapByVersionlessId, ReleaseDescriptor releaseDescriptor) {
        Artifact checkArtifact = getArtifactFromMap(artifact, artifactMapByVersionlessId);

        return checkArtifact(checkArtifact, releaseDescriptor);
    }

    private static Artifact getArtifactFromMap(Artifact artifact, Map<String, Artifact> artifactMapByVersionlessId) {
        String versionlessId = ArtifactUtils.versionlessKey(artifact);
        Artifact checkArtifact = artifactMapByVersionlessId.get(versionlessId);

        if (checkArtifact == null) {
            checkArtifact = artifact;
        }
        return checkArtifact;
    }

    private static boolean checkArtifact(Artifact artifact, ReleaseDescriptor releaseDescriptor) {
        String versionlessKey = ArtifactUtils.versionlessKey(artifact.getGroupId(), artifact.getArtifactId());
        String releaseDescriptorResolvedVersion = releaseDescriptor.getDependencyReleaseVersion(versionlessKey);

        boolean releaseDescriptorResolvedVersionIsSnapshot = releaseDescriptorResolvedVersion == null
                || releaseDescriptorResolvedVersion.contains(Artifact.SNAPSHOT_VERSION);

        // We are only looking at dependencies external to the project - ignore anything found in the reactor as
        // it's version will be updated
        boolean bannedVersion = artifact.isSnapshot()
                && !artifact.getBaseVersion().equals(releaseDescriptor.getProjectOriginalVersion(versionlessKey))
                && releaseDescriptorResolvedVersionIsSnapshot;

        // If we have a snapshot but allowTimestampedSnapshots is true, accept the artifact if the version
        // indicates that it is a timestamped snapshot.
        if (bannedVersion && releaseDescriptor.isAllowTimestampedSnapshots()) {
            bannedVersion = artifact.getVersion().contains(Artifact.SNAPSHOT_VERSION);
        }

        return bannedVersion;
    }

    @Override
    public ReleaseResult simulate(
            ReleaseDescriptor releaseDescriptor,
            ReleaseEnvironment releaseEnvironment,
            List<MavenProject> reactorProjects)
            throws ReleaseExecutionException, ReleaseFailureException {
        // It makes no modifications, so simulate is the same as execute
        return execute(releaseDescriptor, releaseEnvironment, reactorProjects);
    }

    private void printSnapshotDependencies(Set<Artifact> snapshotsSet, StringBuilder message) {
        List<Artifact> snapshotsList = new ArrayList<>(snapshotsSet);

        Collections.sort(snapshotsList);

        for (Artifact artifact : snapshotsList) {
            message.append("    ");

            message.append(artifact);

            message.append("\n");
        }
    }

    private void resolveSnapshots(
            Set<Artifact> projectDependencies,
            Set<Artifact> reportDependencies,
            Set<Artifact> extensionDependencies,
            Set<Artifact> pluginDependencies,
            ReleaseDescriptor releaseDescriptor)
            throws ReleaseExecutionException {
        try {
            String autoResolveSnapshots = releaseDescriptor.getAutoResolveSnapshots();
            if (resolveSnapshot == null) {
                prompter.get().showMessage(RESOLVE_SNAPSHOT_MESSAGE);
                if (autoResolveSnapshots != null) {
                    resolveSnapshot = "yes";
                    prompter.get().showMessage(RESOLVE_SNAPSHOT_PROMPT + " " + resolveSnapshot);
                } else {
                    resolveSnapshot = prompter.get().prompt(RESOLVE_SNAPSHOT_PROMPT, Arrays.asList("yes", "no"), "no");
                }
            }

            if (resolveSnapshot.toLowerCase(Locale.ENGLISH).startsWith("y")) {
                if (resolveSnapshotType == null) {
                    prompter.get().showMessage(RESOLVE_SNAPSHOT_TYPE_MESSAGE);
                    int defaultAnswer = -1;
                    if (autoResolveSnapshots != null) {
                        if ("all".equalsIgnoreCase(autoResolveSnapshots)) {
                            defaultAnswer = 0;
                        } else if ("dependencies".equalsIgnoreCase(autoResolveSnapshots)) {
                            defaultAnswer = 1;
                        } else if ("plugins".equalsIgnoreCase(autoResolveSnapshots)) {
                            defaultAnswer = 2;
                        } else if ("reports".equalsIgnoreCase(autoResolveSnapshots)) {
                            defaultAnswer = 3;
                        } else if ("extensions".equalsIgnoreCase(autoResolveSnapshots)) {
                            defaultAnswer = 4;
                        } else {
                            try {
                                defaultAnswer = Integer.parseInt(autoResolveSnapshots);
                            } catch (NumberFormatException e) {
                                throw new ReleaseExecutionException(e.getMessage(), e);
                            }
                        }
                    }
                    if (defaultAnswer >= 0 && defaultAnswer <= 4) {
                        prompter.get().showMessage(RESOLVE_SNAPSHOT_TYPE_PROMPT + " " + autoResolveSnapshots);
                        resolveSnapshotType = Integer.toString(defaultAnswer);
                    } else {
                        resolveSnapshotType = prompter.get()
                                .prompt(RESOLVE_SNAPSHOT_TYPE_PROMPT, Arrays.asList("0", "1", "2", "3"), "1");
                    }
                }

                switch (Integer.parseInt(resolveSnapshotType.toLowerCase(Locale.ENGLISH))) {
                        // all
                    case 0:
                        processSnapshot(projectDependencies, releaseDescriptor, autoResolveSnapshots);
                        processSnapshot(pluginDependencies, releaseDescriptor, autoResolveSnapshots);
                        processSnapshot(reportDependencies, releaseDescriptor, autoResolveSnapshots);
                        processSnapshot(extensionDependencies, releaseDescriptor, autoResolveSnapshots);
                        break;

                        // project dependencies
                    case 1:
                        processSnapshot(projectDependencies, releaseDescriptor, autoResolveSnapshots);
                        break;

                        // plugins
                    case 2:
                        processSnapshot(pluginDependencies, releaseDescriptor, autoResolveSnapshots);
                        break;

                        // reports
                    case 3:
                        processSnapshot(reportDependencies, releaseDescriptor, autoResolveSnapshots);
                        break;

                        // extensions
                    case 4:
                        processSnapshot(extensionDependencies, releaseDescriptor, autoResolveSnapshots);
                        break;

                    default:
                }
            }
        } catch (PrompterException | VersionParseException e) {
            throw new ReleaseExecutionException(e.getMessage(), e);
        }
    }

    private void processSnapshot(
            Set<Artifact> snapshotSet, ReleaseDescriptor releaseDescriptor, String autoResolveSnapshots)
            throws PrompterException, VersionParseException {
        Iterator<Artifact> iterator = snapshotSet.iterator();

        while (iterator.hasNext()) {
            Artifact currentArtifact = iterator.next();
            String versionlessKey = ArtifactUtils.versionlessKey(currentArtifact);

            VersionInfo versionInfo = new DefaultVersionInfo(currentArtifact.getBaseVersion());
            releaseDescriptor.addDependencyOriginalVersion(versionlessKey, versionInfo.toString());

            prompter.get()
                    .showMessage("Dependency '" + versionlessKey + "' is a snapshot (" + currentArtifact.getVersion()
                            + ")\n");
            String message = "Which release version should it be set to?";
            String result;
            if (null != autoResolveSnapshots) {
                result = versionInfo.getReleaseVersionString();
                prompter.get().showMessage(message + " " + result);
            } else {
                result = prompter.get().prompt(message, versionInfo.getReleaseVersionString());
            }

            releaseDescriptor.addDependencyReleaseVersion(versionlessKey, result);

            iterator.remove();

            // by default, keep the same version for the dependency after release, unless it was previously newer
            // the user may opt to type in something different
            VersionInfo nextVersionInfo = new DefaultVersionInfo(result);

            String nextVersion;
            if (nextVersionInfo.compareTo(versionInfo) > 0) {
                nextVersion = nextVersionInfo.toString();
            } else {
                nextVersion = versionInfo.toString();
            }

            message = "What version should the dependency be reset to for development?";
            if (null != autoResolveSnapshots) {
                result = nextVersion;
                prompter.get().showMessage(message + " " + result);
            } else {
                result = prompter.get().prompt(message, nextVersion);
            }

            releaseDescriptor.addDependencyDevelopmentVersion(versionlessKey, result);
        }
    }
}
