/**
 * 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.storm.submit.command;

import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;

import java.io.File;
import java.io.FileNotFoundException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.lang.StringUtils;
import org.apache.storm.submit.dependency.AetherUtils;
import org.apache.storm.submit.dependency.DependencyResolver;
import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.graph.Dependency;
import org.eclipse.aether.repository.Proxy;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.resolution.ArtifactResult;
import org.eclipse.aether.util.repository.AuthenticationBuilder;
import org.json.simple.JSONValue;

/**
 * Main class of dependency resolver.
 */
public class DependencyResolverMain {
    private static final String OPTION_ARTIFACTS_LONG = "artifacts";
    private static final String OPTION_ARTIFACT_REPOSITORIES_LONG = "artifactRepositories";
    private static final String OPTION_MAVEN_LOCAL_REPOSITORY_DIRECTORY_LONG = "mavenLocalRepositoryDirectory";
    private static final String OPTION_PROXY_URL_LONG = "proxyUrl";
    private static final String OPTION_PROXY_USERNAME_LONG = "proxyUsername";
    private static final String OPTION_PROXY_PASSWORD_LONG = "proxyPassword";
    public static final String DEFAULT_FAILBACK_MAVEN_LOCAL_REPOSITORY_DIRECTORY = "local-repo";

    /**
     * Main entry of dependency resolver.
     *
     * @param args console arguments
     * @throws ParseException If there's parsing error on option parse.
     * @throws MalformedURLException If proxy URL is malformed.
     */
    public static void main(String[] args) throws ParseException, MalformedURLException {
        Options options = buildOptions();
        CommandLineParser parser = new DefaultParser();
        CommandLine commandLine = parser.parse(options, args);

        if (!commandLine.hasOption(OPTION_ARTIFACTS_LONG)) {
            throw new IllegalArgumentException("artifacts must be presented.");
        }

        String artifactsArg = commandLine.getOptionValue(OPTION_ARTIFACTS_LONG);

        // DO NOT CHANGE THIS TO SYSOUT
        System.err.println("DependencyResolver input - artifacts: " + artifactsArg);
        List<Dependency> dependencies = parseArtifactArgs(artifactsArg);

        List<RemoteRepository> repositories;
        if (commandLine.hasOption(OPTION_ARTIFACT_REPOSITORIES_LONG)) {
            String remoteRepositoryArg = commandLine.getOptionValue(OPTION_ARTIFACT_REPOSITORIES_LONG);

            // DO NOT CHANGE THIS TO SYSOUT
            System.err.println("DependencyResolver input - repositories: " + remoteRepositoryArg);

            repositories = parseRemoteRepositoryArgs(remoteRepositoryArg);
        } else {
            repositories = Collections.emptyList();
        }

        try {
            String localMavenRepoPath = getOrDefaultLocalMavenRepositoryPath(
                    commandLine.getOptionValue(OPTION_MAVEN_LOCAL_REPOSITORY_DIRECTORY_LONG),
                    DEFAULT_FAILBACK_MAVEN_LOCAL_REPOSITORY_DIRECTORY);

            // create root directory if not exist
            Files.createDirectories(new File(localMavenRepoPath).toPath());

            DependencyResolver resolver = new DependencyResolver(localMavenRepoPath, repositories);

            if (commandLine.hasOption(OPTION_PROXY_URL_LONG)) {
                String proxyUrl = commandLine.getOptionValue(OPTION_PROXY_URL_LONG);
                String proxyUsername = commandLine.getOptionValue(OPTION_PROXY_USERNAME_LONG);
                String proxyPassword = commandLine.getOptionValue(OPTION_PROXY_PASSWORD_LONG);

                resolver.setProxy(parseProxyArg(proxyUrl, proxyUsername, proxyPassword));
            }

            List<ArtifactResult> artifactResults = resolver.resolve(dependencies);

            Iterable<ArtifactResult> missingArtifacts = filterMissingArtifacts(artifactResults);
            if (missingArtifacts.iterator().hasNext()) {
                printMissingArtifactsToSysErr(missingArtifacts);
                throw new RuntimeException("Some artifacts are not resolved");
            }

            System.out.println(JSONValue.toJSONString(transformArtifactResultToArtifactToPaths(artifactResults)));
            System.out.flush();
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }

    private static Iterable<ArtifactResult> filterMissingArtifacts(List<ArtifactResult> artifactResults) {
        return Iterables.filter(artifactResults, new Predicate<ArtifactResult>() {
            @Override
            public boolean apply(ArtifactResult artifactResult) {
                return artifactResult.isMissing();
            }
        });
    }

    private static void printMissingArtifactsToSysErr(Iterable<ArtifactResult> missingArtifacts) {
        for (ArtifactResult artifactResult : missingArtifacts) {
            System.err.println("ArtifactResult : " + artifactResult + " / Errors : " + artifactResult.getExceptions());
        }
    }

    private static List<Dependency> parseArtifactArgs(String artifactArgs) {
        List<String> artifacts = Arrays.asList(artifactArgs.split(","));
        List<Dependency> dependencies = new ArrayList<>(artifacts.size());
        for (String artifactOpt : artifacts) {
            if (artifactOpt.trim().isEmpty()) {
                continue;
            }

            dependencies.add(AetherUtils.parseDependency(artifactOpt));
        }

        return dependencies;
    }

    private static List<RemoteRepository> parseRemoteRepositoryArgs(String remoteRepositoryArg) {
        List<String> repositories = Arrays.asList(remoteRepositoryArg.split(","));
        List<RemoteRepository> remoteRepositories = new ArrayList<>(repositories.size());
        for (String repositoryOpt : repositories) {
            if (repositoryOpt.trim().isEmpty()) {
                continue;
            }

            remoteRepositories.add(AetherUtils.parseRemoteRepository(repositoryOpt));
        }

        return remoteRepositories;
    }

    private static Proxy parseProxyArg(String proxyUrl, String proxyUsername, String proxyPassword) throws MalformedURLException {
        URL url = new URL(proxyUrl);
        if (StringUtils.isNotEmpty(proxyUsername) && StringUtils.isNotEmpty(proxyPassword)) {
            AuthenticationBuilder authBuilder = new AuthenticationBuilder();
            authBuilder.addUsername(proxyUsername).addPassword(proxyPassword);
            return new Proxy(url.getProtocol(), url.getHost(), url.getPort(), authBuilder.build());
        } else {
            return new Proxy(url.getProtocol(), url.getHost(), url.getPort());
        }
    }

    private static Map<String, String> transformArtifactResultToArtifactToPaths(List<ArtifactResult> artifactResults) {
        Map<String, String> artifactToPath = new LinkedHashMap<>();
        for (ArtifactResult artifactResult : artifactResults) {
            Artifact artifact = artifactResult.getArtifact();
            artifactToPath.put(AetherUtils.artifactToString(artifact), artifact.getFile().getAbsolutePath());
        }
        return artifactToPath;
    }

    private static String getOrDefaultLocalMavenRepositoryPath(String customLocalMavenPath, String defaultPath) {
        if (customLocalMavenPath != null) {
            Path customPath = new File(customLocalMavenPath).toPath();
            Preconditions.checkArgument(!Files.exists(customPath) || Files.isDirectory(customPath),
                    "Custom local maven repository path exist and is not a directory!");
            return customLocalMavenPath;
        }

        String localMavenRepoPathStr = getLocalMavenRepositoryPath();
        if (StringUtils.isNotEmpty(localMavenRepoPathStr)) {
            Path localMavenRepoPath = new File(localMavenRepoPathStr).toPath();
            if (Files.exists(localMavenRepoPath) && Files.isDirectory(localMavenRepoPath)) {
                return localMavenRepoPathStr;
            }
        }

        return defaultPath;
    }

    private static String getLocalMavenRepositoryPath() {
        String userHome = System.getProperty("user.home");
        if (StringUtils.isNotEmpty(userHome)) {
            return userHome + File.separator + ".m2" + File.separator + "repository";
        }

        return null;
    }

    private static Options buildOptions() {
        Options options = new Options();
        options.addOption(null, OPTION_ARTIFACTS_LONG, true, "REQUIRED string representation of artifacts");
        options.addOption(null, OPTION_ARTIFACT_REPOSITORIES_LONG, true, "OPTIONAL string representation of artifact repositories");
        options.addOption(null, OPTION_MAVEN_LOCAL_REPOSITORY_DIRECTORY_LONG, true, "OPTIONAL string representation of local maven repository directory path");
        options.addOption(null, OPTION_PROXY_URL_LONG, true, "OPTIONAL URL representation of proxy server");
        options.addOption(null, OPTION_PROXY_USERNAME_LONG, true, "OPTIONAL Username of proxy server (basic auth)");
        options.addOption(null, OPTION_PROXY_PASSWORD_LONG, true, "OPTIONAL Password of proxy server (basic auth)");
        return options;
    }
}
