/*
 * Copyright The WildFly Authors
 * SPDX-License-Identifier: Apache-2.0
 */
package org.jboss.as.model.test;

import static org.wildfly.common.Assert.checkNotNullParam;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.maven.repository.internal.DefaultArtifactDescriptorReader;
import org.apache.maven.repository.internal.DefaultVersionRangeResolver;
import org.apache.maven.repository.internal.DefaultVersionResolver;
import org.apache.maven.repository.internal.MavenRepositorySystemUtils;
import org.apache.maven.repository.internal.SnapshotMetadataGeneratorFactory;
import org.apache.maven.repository.internal.VersionsMetadataGeneratorFactory;
import org.eclipse.aether.AbstractRepositoryListener;
import org.eclipse.aether.DefaultRepositorySystemSession;
import org.eclipse.aether.RepositoryEvent;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.eclipse.aether.collection.CollectRequest;
import org.eclipse.aether.collection.DependencyCollectionException;
import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory;
import org.eclipse.aether.graph.Dependency;
import org.eclipse.aether.graph.DependencyFilter;
import org.eclipse.aether.graph.DependencyNode;
import org.eclipse.aether.impl.ArtifactDescriptorReader;
import org.eclipse.aether.impl.DefaultServiceLocator;
import org.eclipse.aether.impl.MetadataGeneratorFactory;
import org.eclipse.aether.impl.VersionRangeResolver;
import org.eclipse.aether.impl.VersionResolver;
import org.eclipse.aether.repository.LocalRepository;
import org.eclipse.aether.repository.Proxy;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.resolution.ArtifactRequest;
import org.eclipse.aether.resolution.ArtifactResolutionException;
import org.eclipse.aether.resolution.ArtifactResult;
import org.eclipse.aether.resolution.DependencyRequest;
import org.eclipse.aether.resolution.DependencyResolutionException;
import org.eclipse.aether.spi.connector.RepositoryConnectorFactory;
import org.eclipse.aether.spi.connector.transport.TransporterFactory;
import org.eclipse.aether.transfer.AbstractTransferListener;
import org.eclipse.aether.transfer.TransferEvent;
import org.eclipse.aether.transfer.TransferResource;
import org.eclipse.aether.transport.file.FileTransporterFactory;
import org.eclipse.aether.transport.http.HttpTransporterFactory;
import org.eclipse.aether.util.filter.ExclusionsDependencyFilter;
import org.eclipse.aether.util.graph.visitor.PreorderNodeListGenerator;
import org.eclipse.aether.util.version.GenericVersionScheme;
import org.eclipse.aether.version.InvalidVersionSpecificationException;
import org.eclipse.aether.version.VersionScheme;
import org.jboss.logging.Logger;

/**
 * @author <a href="kabir.khan@jboss.com">Kabir Khan</a>
 * @author Tomaz Cerar
 */
class MavenUtil {

    private final RepositorySystem REPOSITORY_SYSTEM;
    private final List<RemoteRepository> remoteRepositories;

    private static final String PROXY_HTTP_PREFIX = "http.";
    private static final String PROXY_HTTPS_PREFIX = "https.";
    private static final String PROXY_HOST = "proxyHost";
    private static final String PROXY_PORT = "proxyPort";

    private static MavenSettings mavenSettings = MavenSettings.getSettings();
    private DefaultRepositorySystemSession session;
    private static final Logger log = Logger.getLogger("maven.downloader");

    private MavenUtil(RepositorySystem repositorySystem, List<RemoteRepository> remoteRepositories) {
        this.REPOSITORY_SYSTEM = repositorySystem;
        this.remoteRepositories = remoteRepositories;
    }

    static MavenUtil create(boolean useEapRepository) {

        return new MavenUtil(newRepositorySystem(), createRemoteRepositories(useEapRepository));
    }

    URL createMavenGavURL(String artifactGav) throws MalformedURLException {
        Artifact artifact = new DefaultArtifact(artifactGav);
        checkNotNullParam("artifact.getVersion", artifact.getVersion());

        VersionScheme versionScheme = new GenericVersionScheme();
        try {
            versionScheme.parseVersion(artifact.getVersion());
        } catch (InvalidVersionSpecificationException e) {
            throw new IllegalArgumentException(e);
        }

        try {
            versionScheme.parseVersionRange(artifact.getVersion());
            throw new IllegalArgumentException(artifact.getVersion() + " is a version range. A specific version is needed");
        } catch (InvalidVersionSpecificationException expected) {

        }

        RepositorySystemSession session = newRepositorySystemSession();

        ArtifactRequest artifactRequest = new ArtifactRequest();
        artifactRequest.setArtifact(artifact);
        for (RemoteRepository remoteRepo : remoteRepositories) {
            artifactRequest.addRepository(remoteRepo);
        }

        ArtifactResult artifactResult;
        try {
            artifactResult = REPOSITORY_SYSTEM.resolveArtifact(session, artifactRequest);
        } catch (ArtifactResolutionException e) {
            throw new RuntimeException(e);
        }


        File file = artifactResult.getArtifact().getFile().getAbsoluteFile();
        return file.toURI().toURL();
    }

    List<URL> createMavenGavRecursiveURLs(String artifactGav, String... excludes) throws MalformedURLException, DependencyCollectionException, DependencyResolutionException {
        Artifact artifact = new DefaultArtifact(artifactGav);
        checkNotNullParam("artifact.getVersion", artifact.getVersion());

        VersionScheme versionScheme = new GenericVersionScheme();
        try {
            versionScheme.parseVersion(artifact.getVersion());
        } catch (InvalidVersionSpecificationException e) {
            throw new IllegalArgumentException(e);
        }

        try {
            versionScheme.parseVersionRange(artifact.getVersion());
            throw new IllegalArgumentException(artifact.getVersion() + " is a version range. A specific version is needed");
        } catch (InvalidVersionSpecificationException expected) {

        }

        RepositorySystemSession session = newRepositorySystemSession();

        ArtifactRequest artifactRequest = new ArtifactRequest();
        artifactRequest.setArtifact(artifact);
        for (RemoteRepository remoteRepo : remoteRepositories) {
            artifactRequest.addRepository(remoteRepo);
        }

        ArtifactResult artifactResult;
        try {
            artifactResult = REPOSITORY_SYSTEM.resolveArtifact(session, artifactRequest);
        } catch (ArtifactResolutionException e) {
            throw new RuntimeException(e);
        }

        List<URL> urls = new ArrayList<>();
        urls.add(artifactToUrl(artifactResult.getArtifact()));

        CollectRequest collectRequest = new CollectRequest();
        collectRequest.setRoot(new Dependency(artifact, "compile"));
        for (RemoteRepository remoteRepo : remoteRepositories) {
            collectRequest.addRepository(remoteRepo);
        }

        DependencyNode node = REPOSITORY_SYSTEM.collectDependencies(session, collectRequest).getRoot();
        DependencyFilter filter = new ExclusionsDependencyFilter(Arrays.asList(excludes));
        DependencyRequest dependencyRequest = new DependencyRequest(node, filter);

        REPOSITORY_SYSTEM.resolveDependencies(session, dependencyRequest);

        PreorderNodeListGenerator nlg = new PreorderNodeListGenerator();
        node.accept(nlg);
        for (Artifact cur : nlg.getArtifacts(false)) {
            urls.add(artifactToUrl(cur));
        }

        log.debug("--------------------");
        log.debug(nlg.getClassPath());
        log.debug("--------------------");

        return urls;
    }

    private static Integer getProxyPort(String systemProperty) {
        String port = System.getProperty(systemProperty);
        if (port != null && !port.isEmpty()) {
            try {
                Integer intPort = Integer.parseInt(port);
                return intPort;
            } catch (NumberFormatException e) {
                return null;
            }
        }
        return null;
    }

    private static List<RemoteRepository> createRemoteRepositories(boolean useEapRepository) {
        // prepare proxy
        String httpProxyHost = System.getProperty(String.format("%s%s", PROXY_HTTP_PREFIX, PROXY_HOST));
        String httpsProxyHost = System.getProperty(String.format("%s%s", PROXY_HTTPS_PREFIX, PROXY_HOST));
        Integer httpProxyPort = getProxyPort(String.format("%s%s", PROXY_HTTP_PREFIX, PROXY_PORT));
        Integer httpsProxyPort = getProxyPort(String.format("%s%s", PROXY_HTTPS_PREFIX, PROXY_PORT));
        Proxy httpProxy = null;
        Proxy httpsProxy = null;
        if (httpProxyHost != null && httpProxyPort != null && !httpProxyHost.isEmpty()) {
            httpProxy = new Proxy(Proxy.TYPE_HTTP, httpProxyHost, httpProxyPort);
        }
        if (httpsProxyHost != null && httpsProxyPort != null && !httpsProxyHost.isEmpty()) {
            httpsProxy = new Proxy(Proxy.TYPE_HTTPS, httpsProxyHost, httpsProxyPort);
        }

        String remoteReposFromSysProp = System.getProperty(ChildFirstClassLoaderBuilder.MAVEN_REPOSITORY_URLS);
        List<RemoteRepository> remoteRepositories = new ArrayList<>();
        if (remoteReposFromSysProp == null || remoteReposFromSysProp.trim().length() == 0 || remoteReposFromSysProp.startsWith("${")) {
            if (useEapRepository) {
                RemoteRepository.Builder repository = new RemoteRepository.Builder("product-repository", "default", "https://maven.repository.redhat.com/ga/");
                if (httpsProxy != null) {
                    repository.setProxy(httpsProxy);
                }
                remoteRepositories.add(repository.build());
            }
            //always add jboss developer repository
            RemoteRepository.Builder repository = new RemoteRepository.Builder("jboss-developer", "default", "https://repository.jboss.org/nexus/content/groups/developer/");
            if (httpProxy != null) {
                repository.setProxy(httpProxy);
            }
            remoteRepositories.add(repository.build());
            // add the jboss staging repository only if the system property exists
            if (System.getProperty("jboss.staging.repository.group") != null) {
                RemoteRepository.Builder stagingRepository = new RemoteRepository.Builder("jboss-staging-repository-group", "default", "https://repository.jboss.org/nexus/content/groups/staging/");
                if (httpProxy != null) {
                    stagingRepository.setProxy(httpProxy);
                }
                remoteRepositories.add(stagingRepository.build());
            }
            //add repos from users settings.xml
            List<String> remoteRepositories1 = mavenSettings.getRemoteRepositories();
            for (int i = 0; i < remoteRepositories1.size(); i++) {
                String repo = remoteRepositories1.get(i);
                RemoteRepository.Builder myRepo = new RemoteRepository.Builder("repo-" +i, "default", repo);
                if (httpProxy != null && repo.startsWith("http")) {
                    myRepo.setProxy(httpProxy);
                }
                if (httpsProxy != null && repo.startsWith("https")) {
                    myRepo.setProxy(httpsProxy);
                }
                remoteRepositories.add(myRepo.build());
            }

        } else {
            int i = 0;
            for (String repoUrl : remoteReposFromSysProp.split(",")) {
                //remoteRepositories.add(new RemoteRepository("repo" + i, "default", repoUrl.trim()));
                repoUrl = repoUrl.trim();
                RemoteRepository.Builder repository = new RemoteRepository.Builder("repo" + i, "default", repoUrl);
                if (repoUrl.startsWith("https:") && httpsProxy != null) {
                    repository.setProxy(httpsProxy);
                }
                if (repoUrl.startsWith("http:") && httpProxy != null) {
                    repository.setProxy(httpProxy);
                }
                remoteRepositories.add(repository.build());
                i++;
            }
        }
        return remoteRepositories;
    }

    private RepositorySystemSession newRepositorySystemSession() {
        if (this.session != null){
            return this.session;
        }
        DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession();


        LocalRepository localRepo = new LocalRepository(mavenSettings.getLocalRepository().toString());
        session.setLocalRepositoryManager(REPOSITORY_SYSTEM.newLocalRepositoryManager(session, localRepo));

        //Copy these from the aether demo if they are nice to have
        session.setTransferListener(new ConsoleTransferListener());
        session.setRepositoryListener(new ConsoleRepositoryListener());
        session.setTransferListener(new AbstractTransferListener() {
            @Override
            public void transferFailed(TransferEvent event) {
                super.transferFailed(event);
            }
        });
        this.session = session;
        return session;
    }

    private static URL artifactToUrl(Artifact artifact) throws MalformedURLException {
        return artifact.getFile().toURI().toURL();
    }

    static RepositorySystem newRepositorySystem() {
            /*
             * Aether's components implement
             * org.sonatype.aether.spi.locator.Service to ease manual wiring and
             * using the prepopulated DefaultServiceLocator, we only need to
             * register the repository connector factories.
             */

        DefaultServiceLocator locator = new DefaultServiceLocator();
        locator.addService(ArtifactDescriptorReader.class, DefaultArtifactDescriptorReader.class);
        locator.addService(VersionResolver.class, DefaultVersionResolver.class);
        locator.addService(VersionRangeResolver.class, DefaultVersionRangeResolver.class);
        locator.addService(MetadataGeneratorFactory.class, SnapshotMetadataGeneratorFactory.class);
        locator.addService(MetadataGeneratorFactory.class, VersionsMetadataGeneratorFactory.class);
        locator.setErrorHandler(new MyErrorHandler());

        locator.addService(RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class);
        locator.addService(TransporterFactory.class, FileTransporterFactory.class);
        //locator.addService(TransporterFactory.class, WagonTransporterFactory.class);
        locator.addService(TransporterFactory.class, HttpTransporterFactory.class);


        return locator.getService(RepositorySystem.class);
    }

    static class MyErrorHandler extends DefaultServiceLocator.ErrorHandler {
        @Override
        public void serviceCreationFailed(Class<?> type, Class<?> impl, Throwable exception) {
            log.error("Could not create type: " + type + " impl: " + impl, exception);
        }
    }

    private static class ConsoleRepositoryListener extends AbstractRepositoryListener {

        ConsoleRepositoryListener() {
        }


        public void artifactDeployed(RepositoryEvent event) {
            log.debug("Deployed " + event.getArtifact() + " to " + event.getRepository());
        }

        public void artifactDeploying(RepositoryEvent event) {
            log.debug("Deploying " + event.getArtifact() + " to " + event.getRepository());
        }

        public void artifactDescriptorInvalid(RepositoryEvent event) {
            log.debug("Invalid artifact descriptor for " + event.getArtifact() + ": " ,event.getException());
        }

        public void artifactDescriptorMissing(RepositoryEvent event) {
            log.debug("Missing artifact descriptor for " + event.getArtifact());
        }

        public void artifactInstalled(RepositoryEvent event) {
            log.debug("Installed " + event.getArtifact() + " to " + event.getFile());
        }

        public void artifactInstalling(RepositoryEvent event) {
            log.debug("Installing " + event.getArtifact() + " to " + event.getFile());
        }

        public void artifactResolved(RepositoryEvent event) {
            log.debug("Resolved artifact " + event.getArtifact() + " from " + event.getRepository());
        }

        public void artifactDownloading(RepositoryEvent event) {
            log.debug("Downloading artifact " + event.getArtifact() + " from " + event.getRepository());
        }

        public void artifactDownloaded(RepositoryEvent event) {
            log.debug("Downloaded artifact " + event.getArtifact() + " from " + event.getRepository());
        }

        public void artifactResolving(RepositoryEvent event) {
            log.debug("Resolving artifact " + event.getArtifact());
        }

        public void metadataDeployed(RepositoryEvent event) {
            log.debug("Deployed " + event.getMetadata() + " to " + event.getRepository());
        }

        public void metadataDeploying(RepositoryEvent event) {
            log.debug("Deploying " + event.getMetadata() + " to " + event.getRepository());
        }

        public void metadataInstalled(RepositoryEvent event) {
            log.debug("Installed " + event.getMetadata() + " to " + event.getFile());
        }

        public void metadataInstalling(RepositoryEvent event) {
            log.debug("Installing " + event.getMetadata() + " to " + event.getFile());
        }

        public void metadataInvalid(RepositoryEvent event) {
            log.debug("Invalid metadata " + event.getMetadata());
        }

        public void metadataResolved(RepositoryEvent event) {
            log.debug("Resolved metadata " + event.getMetadata() + " from " + event.getRepository());
        }

        public void metadataResolving(RepositoryEvent event) {
            log.debug("Resolving metadata " + event.getMetadata() + " from " + event.getRepository());
        }

    }

    private static class ConsoleTransferListener extends AbstractTransferListener {

        private Map<TransferResource, Long> downloads = new ConcurrentHashMap<>();
        private int lastLength;

        ConsoleTransferListener() {

        }


        @Override
        public void transferInitiated(TransferEvent event) {
            String message = event.getRequestType() == TransferEvent.RequestType.PUT ? "Uploading" : "Downloading";

            log.debug(message + ": " + event.getResource().getRepositoryUrl() + event.getResource().getResourceName());
        }

        @Override
        public void transferProgressed(TransferEvent event) {
            TransferResource resource = event.getResource();
            downloads.put(resource, Long.valueOf(event.getTransferredBytes()));

            StringBuilder buffer = new StringBuilder(64);

            for (Map.Entry<TransferResource, Long> entry : downloads.entrySet()) {
                long total = entry.getKey().getContentLength();
                long complete = entry.getValue().longValue();

                buffer.append(getStatus(complete, total)).append("  ");
            }

            int pad = lastLength - buffer.length();
            lastLength = buffer.length();
            pad(buffer, pad);
            buffer.append('\r');
            log.trace(buffer);
        }

        private String getStatus(long complete, long total) {
            if (total >= 1024) {
                return toKB(complete) + "/" + toKB(total) + " KB ";
            } else if (total >= 0) {
                return complete + "/" + total + " B ";
            } else if (complete >= 1024) {
                return toKB(complete) + " KB ";
            } else {
                return complete + " B ";
            }
        }

        private void pad(StringBuilder buffer, int spaces) {
            String block = "                                        ";
            while (spaces > 0) {
                int n = Math.min(spaces, block.length());
                buffer.append(block, 0, n);
                spaces -= n;
            }
        }

        @Override
        public void transferSucceeded(TransferEvent event) {
            transferCompleted(event);

            TransferResource resource = event.getResource();
            long contentLength = event.getTransferredBytes();
            if (contentLength >= 0) {
                String type = (event.getRequestType() == TransferEvent.RequestType.PUT ? "Uploaded" : "Downloaded");
                String len = contentLength >= 1024 ? toKB(contentLength) + " KB" : contentLength + " B";

                String throughput = "";
                long duration = System.currentTimeMillis() - resource.getTransferStartTime();
                if (duration > 0) {
                    DecimalFormat format = new DecimalFormat("0.0", new DecimalFormatSymbols(Locale.ENGLISH));
                    double kbPerSec = (contentLength / 1024.0) / (duration / 1000.0);
                    throughput = " at " + format.format(kbPerSec) + " KB/sec";
                }

                log.debug(type + ": " + resource.getRepositoryUrl() + resource.getResourceName() + " (" + len + throughput
                        + ")");
            }
        }

        @Override
        public void transferFailed(TransferEvent event) {
            transferCompleted(event);
            log.warn(event.getException());

        }

        private void transferCompleted(TransferEvent event) {
            downloads.remove(event.getResource());

            StringBuilder buffer = new StringBuilder(64);
            pad(buffer, lastLength);
            buffer.append('\r');
            log.trace(buffer);

        }

        public void transferCorrupted(TransferEvent event) {
            log.debug(event.getException());
        }

        long toKB(long bytes) {
            return (bytes + 1023) / 1024;
        }

    }
}
