package gate.util.maven;

import java.io.File;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;

import gate.util.GateRuntimeException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.maven.repository.internal.MavenRepositorySystemUtils;
import org.apache.maven.settings.*;
import org.apache.maven.settings.building.DefaultSettingsBuilder;
import org.apache.maven.settings.building.DefaultSettingsBuilderFactory;
import org.apache.maven.settings.building.DefaultSettingsBuildingRequest;
import org.apache.maven.settings.building.SettingsBuildingException;
import org.apache.maven.settings.building.SettingsBuildingRequest;
import org.apache.maven.settings.building.SettingsBuildingResult;
import org.eclipse.aether.DefaultRepositorySystemSession;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory;
import org.eclipse.aether.impl.DefaultServiceLocator;
import org.eclipse.aether.repository.LocalRepository;
import org.eclipse.aether.repository.Proxy;
import org.eclipse.aether.repository.ProxySelector;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.repository.WorkspaceReader;
import org.eclipse.aether.spi.connector.RepositoryConnectorFactory;
import org.eclipse.aether.spi.connector.transport.TransporterFactory;
import org.eclipse.aether.transport.file.FileTransporterFactory;
import org.eclipse.aether.transport.http.HttpTransporterFactory;
import org.eclipse.aether.util.repository.*;
import org.sonatype.plexus.components.cipher.DefaultPlexusCipher;
import org.sonatype.plexus.components.cipher.PlexusCipherException;
import org.sonatype.plexus.components.sec.dispatcher.DefaultSecDispatcher;
import org.sonatype.plexus.components.sec.dispatcher.SecDispatcher;
import org.sonatype.plexus.components.sec.dispatcher.SecDispatcherException;

public class Utils {
  
  private static final Logger log = LoggerFactory.getLogger(Utils.class);
  
  public static final String userHome = System.getProperty("user.home");

  public static final File userMavenConfigurationHome =
          new File(userHome, ".m2");

  public static final String envM2Home = System.getenv("M2_HOME");

  public static final File DEFAULT_USER_SETTINGS_FILE =
          new File(userMavenConfigurationHome, "settings.xml");

  public static final String settingsXml = System.getProperty(
          "M2_SETTINGS_XML", DEFAULT_USER_SETTINGS_FILE.getPath());

  public static final File DEFAULT_GLOBAL_SETTINGS_FILE =
          new File(
                  System.getProperty("maven.home",
                          envM2Home != null ? envM2Home : ""),
                  "conf/settings.xml");

  /**
   * Utility used to decrypt encrypted proxy passwords in settings.xml
   */
  public static final SecDispatcher PASSWORD_DECRYPTER = new DefaultSecDispatcher() {
    {
      _configurationFile = "~/.m2/settings-security.xml";
      try {
        _cipher = new DefaultPlexusCipher();
      } catch (PlexusCipherException e) {
        throw new RuntimeException(e);
      }
    }
  };
  
  private static List<File> extraCacheDirectories = new CopyOnWriteArrayList<>();
  
  /**
   * A list of extra workspace cache directories that should be
   * used when resolving Maven plugins.
   * 
   * @return an unmodifiable list of the current registered caches
   */
  public static List<File> getExtraCacheDirectories() {
    return Collections.unmodifiableList(extraCacheDirectories);
  }
  
  /**
   * Add an extra cache directory to be used when resolving Maven plugins.
   * Caches registered in this way will be searched after the default
   * caches alongside saved GATE applications.
   * @param dir the cache directory to add
   */
  public static void addCacheDirectory(File dir) {
    extraCacheDirectories.add(0, dir);
  }
  
  /**
   * Remove a directory from the list of extra caches to be used when
   * resolving Maven plugins.
   * @param dir the cache directory to remove
   * @return <code>true</code> if the directory was removed,
   * <code>false</code> if not (which may mean it wasn't in the
   * list to start with)
   */
  public static boolean removeCacheDirectory(File dir) {
    return extraCacheDirectories.remove(dir);
  }
  
  public static Settings loadMavenSettings()
      throws SettingsBuildingException {
    // http://stackoverflow.com/questions/27818659/loading-mavens-settings-xml-for-jcabi-aether-to-use
    SettingsBuildingRequest settingsBuildingRequest =
        new DefaultSettingsBuildingRequest();
    settingsBuildingRequest.setSystemProperties(System.getProperties());
    settingsBuildingRequest.setUserSettingsFile(new File(settingsXml));
    settingsBuildingRequest.setGlobalSettingsFile(DEFAULT_GLOBAL_SETTINGS_FILE);

    SettingsBuildingResult settingsBuildingResult;
    DefaultSettingsBuilderFactory mvnSettingBuilderFactory =
        new DefaultSettingsBuilderFactory();
    DefaultSettingsBuilder settingsBuilder =
        mvnSettingBuilderFactory.newInstance();
    settingsBuildingResult = settingsBuilder.build(settingsBuildingRequest);

    Settings effectiveSettings = settingsBuildingResult.getEffectiveSettings();
    return effectiveSettings;
  }
  
  public static List<RemoteRepository> getRepositoryList() throws SettingsBuildingException {
    
    // Add all repos from settings.xml
    // http://stackoverflow.com/questions/27818659/loading-mavens-settings-xml-for-jcabi-aether-to-use
    Settings effectiveSettings = loadMavenSettings();

    List<RemoteRepository> repos = new ArrayList<RemoteRepository>();

    RemoteRepository central =
            new RemoteRepository.Builder("central", "default",
                    "https://repo1.maven.org/maven2/").build();

    // Without this we wouldn't be able to find SNAPSHOT builds of plugins we
    // haven't built and installed locally ourselves
    RemoteRepository gateRepo = new RemoteRepository.Builder("gate", "default",
            "http://repo.gate.ac.uk/content/groups/public/").build();

    DefaultMirrorSelector mirrorSelector = null;
    List<Mirror> mirrors = effectiveSettings.getMirrors();
    if(!mirrors.isEmpty()) {
      mirrorSelector = new DefaultMirrorSelector();
      for (Mirror mirror : mirrors) mirrorSelector.add(
              String.valueOf(mirror.getId()), mirror.getUrl(), mirror.getLayout(), false,
              mirror.getMirrorOf(), mirror.getMirrorOfLayouts());

      // replace central and gate repos with their mirrors, if any
      RemoteRepository centralMirror = mirrorSelector.getMirror(central);
      if(centralMirror != null) central = centralMirror;

      RemoteRepository gateMirror = mirrorSelector.getMirror(gateRepo);
      if(gateMirror != null) gateRepo = gateMirror;
    }

    List<org.apache.maven.settings.Proxy> proxies =
        effectiveSettings.getProxies().stream().filter((p) -> p.isActive())
            .collect(Collectors.toList());
    
    
    DefaultProxySelector defaultSelector = null;
    if(!proxies.isEmpty()) {
      defaultSelector = new DefaultProxySelector();
      for (org.apache.maven.settings.Proxy proxy : proxies) {
        try {
          defaultSelector.add(
              new Proxy(proxy.getProtocol(), proxy.getHost(), proxy.getPort(),
                  new AuthenticationBuilder().addUsername(proxy.getUsername())
                      .addPassword(PASSWORD_DECRYPTER.decrypt(proxy.getPassword())).build()),
              proxy.getNonProxyHosts());
        } catch(SecDispatcherException e) {
          throw new GateRuntimeException("Unable to decrypt password for proxy " + proxy.getProtocol() + "://" + proxy.getHost() + ":" + proxy.getPort());
        }
      }
    }
    
    JreProxySelector jreSelector = new JreProxySelector();    
        
    Map<String, Profile> profilesMap = effectiveSettings.getProfilesAsMap();
    for(String profileName : effectiveSettings.getActiveProfiles()) {
      Profile profile = profilesMap.get(profileName);
      List<Repository> repositories = profile.getRepositories();
      for(Repository repo : repositories) {
        RemoteRepository remoteRepo =
                new RemoteRepository.Builder(repo.getId(), "default",
                        repo.getUrl()).build();

        if(mirrorSelector !=  null) {
          // try and find a mirror for this repo
          RemoteRepository mirrorRepo = mirrorSelector.getMirror(remoteRepo);
          if(mirrorRepo != null) {
            remoteRepo = mirrorRepo;
          }
        }

        Proxy proxy = getProxy(remoteRepo, defaultSelector, jreSelector);
        if(proxy != null) {
          remoteRepo = new RemoteRepository.Builder(remoteRepo)
              .setProxy(proxy).build();
        }
        
        repos.add(remoteRepo);
      }
    }
    
    Proxy proxy = getProxy(central, defaultSelector, jreSelector);
    
    if(proxy != null) {
      central = new RemoteRepository.Builder(central)
          .setProxy(proxy).build();
    }
     
    proxy = getProxy(gateRepo, defaultSelector, jreSelector);
    
    if (proxy != null) {
      gateRepo = new RemoteRepository.Builder(gateRepo)
          .setProxy(proxy).build();
    }
    
    repos.add(central);    
    repos.add(gateRepo);

    // now apply authentication settings to all repositories
    ListIterator<RemoteRepository> repoIter = repos.listIterator();
    while(repoIter.hasNext()) {
      RemoteRepository remoteRepo = repoIter.next();
      Server server = effectiveSettings.getServer(remoteRepo.getId());
      if(server != null) {
        // replace this repo with one that has authentication configured
        try {
          repoIter.set(new RemoteRepository.Builder(remoteRepo)
                  .setAuthentication(new AuthenticationBuilder()
                          .addUsername(server.getUsername()).addPassword(PASSWORD_DECRYPTER.decrypt(server.getPassword()))
                          .addPrivateKey(server.getPrivateKey(), PASSWORD_DECRYPTER.decrypt(server.getPassphrase())).build())
                  .build());
        } catch(SecDispatcherException e) {
          throw new GateRuntimeException("Unable to decrypt password/passphrase for server " + server.getId());
        }
      }

    }

    return repos;
  }
  
  private static Proxy getProxy(RemoteRepository repo, ProxySelector ... selectors) {
    Proxy proxy = null;
    
    for (ProxySelector selector : selectors) {
      if (selector != null) {
        proxy = selector.getProxy(repo);
      }
      
      if (proxy != null) return proxy;
    }
    
    return proxy;
  }
  
  public Artifact getArtifact() {
    return null;
  }
  
  public static RepositorySystem getRepositorySystem() {

    DefaultServiceLocator locator =
            MavenRepositorySystemUtils.newServiceLocator();
    locator.addService(RepositoryConnectorFactory.class,
            BasicRepositoryConnectorFactory.class);
    locator.addService(TransporterFactory.class, FileTransporterFactory.class);
    locator.addService(TransporterFactory.class, HttpTransporterFactory.class);

    return locator.getService(RepositorySystem.class);
  }

  public static DefaultRepositorySystemSession getRepositorySession(RepositorySystem repoSystem, WorkspaceReader workspace) {
    
    DefaultRepositorySystemSession repoSystemSession = MavenRepositorySystemUtils.newSession();
    
    String repoLocation = System.getProperty("user.home") + File.separator
            + ".m2" + File.separator + "repository/";
    ChainedProxySelector proxySelector = new ChainedProxySelector();
    try {
      Settings effectiveSettings = loadMavenSettings();
      if(effectiveSettings.getLocalRepository() != null) {
        repoLocation = effectiveSettings.getLocalRepository();
      }

      List<Mirror> mirrors = effectiveSettings.getMirrors();
      if(!mirrors.isEmpty()) {
        DefaultMirrorSelector mirrorSelector = new DefaultMirrorSelector();
        for (Mirror mirror : mirrors) mirrorSelector.add(
                String.valueOf(mirror.getId()), mirror.getUrl(), mirror.getLayout(), false,
                mirror.getMirrorOf(), mirror.getMirrorOfLayouts());
        repoSystemSession.setMirrorSelector(mirrorSelector);
      }

      List<Server> servers = effectiveSettings.getServers();
      if(!servers.isEmpty()) {
        DefaultAuthenticationSelector selector = new DefaultAuthenticationSelector();
        for (Server server : servers) {
          AuthenticationBuilder auth = new AuthenticationBuilder();
          auth.addUsername(server.getUsername()).addPassword(PASSWORD_DECRYPTER.decrypt(server.getPassword()));
          auth.addPrivateKey(server.getPrivateKey(), PASSWORD_DECRYPTER.decrypt(server.getPassphrase()));
          selector.add(server.getId(), auth.build());
        }
        repoSystemSession.setAuthenticationSelector(new ConservativeAuthenticationSelector(selector));
      }

      // extract any proxies configured in the settings - we need to pass these
      // on so that any repositories declared in a dependency POM file can be
      // accessed through the proxy too.
      List<org.apache.maven.settings.Proxy> proxies =
          effectiveSettings.getProxies().stream().filter((p) -> p.isActive())
              .collect(Collectors.toList());
      
      if(!proxies.isEmpty()) {
        DefaultProxySelector defaultSelector = new DefaultProxySelector();
        for (org.apache.maven.settings.Proxy proxy : proxies) {
          defaultSelector.add(
              new Proxy(proxy.getProtocol(), proxy.getHost(), proxy.getPort(),
                  new AuthenticationBuilder().addUsername(proxy.getUsername())
                      .addPassword(PASSWORD_DECRYPTER.decrypt(proxy.getPassword())).build()),
              proxy.getNonProxyHosts());
        }

        proxySelector.addSelector(defaultSelector);
      }

      // pass through the "offline" setting
      repoSystemSession.setOffline(effectiveSettings.isOffline());
    } catch(SettingsBuildingException | SecDispatcherException | RuntimeException e) {
      log.warn(
              "Unable to load Maven settings, using default repository location, and no mirrors, proxy or authentication settings.",
              e);
    }

    LocalRepository localRepo = new LocalRepository(repoLocation);
    log.debug("Using local repository at: " + repoLocation);
    repoSystemSession.setLocalRepositoryManager(repoSystem
            .newLocalRepositoryManager(repoSystemSession, localRepo));
    
    //repoSystemSession.setWorkspaceReader(new SimpleMavenCache(new File("repo")));      
    if (workspace != null) repoSystemSession.setWorkspaceReader(workspace);

    // try JRE proxies after any configured in settings
    proxySelector.addSelector(new JreProxySelector());

    // set proxy selector for any repositories discovered in dependency poms
    repoSystemSession.setProxySelector(proxySelector);

    return repoSystemSession;
  }
}