/*
 * Copyright 2015 Norbert Potocki (norbert.potocki@nort.pl)
 *
 * Licensed 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.cfg4j.source.git;

import static java.util.Objects.requireNonNull;

import org.cfg4j.source.ConfigurationSource;
import org.cfg4j.source.SourceCommunicationException;
import org.cfg4j.source.context.environment.Environment;
import org.cfg4j.source.context.environment.MissingEnvironmentException;
import org.cfg4j.source.context.filesprovider.ConfigFilesProvider;
import org.cfg4j.source.context.propertiesprovider.PropertiesProvider;
import org.cfg4j.source.context.propertiesprovider.PropertiesProviderSelector;
import org.cfg4j.utils.FileUtils;
import org.eclipse.jgit.api.CheckoutCommand;
import org.eclipse.jgit.api.CreateBranchCommand;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.Ref;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.Closeable;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

/**
 * Note: use {@link GitConfigurationSourceBuilder} for building instances of this class.
 * <p>
 * Read configuration from the remote GIT repository. Keeps a local clone of the repository.
 */
class GitConfigurationSource implements ConfigurationSource, Closeable {

  private static final Logger LOG = LoggerFactory.getLogger(GitConfigurationSource.class);

  private final BranchResolver branchResolver;
  private final PathResolver pathResolver;
  private final ConfigFilesProvider configFilesProvider;
  private final PropertiesProviderSelector propertiesProviderSelector;
  private final String repositoryURI;
  private final Path tmpPath;
  private final String tmpRepoPrefix;
  private Git clonedRepo;
  private Path clonedRepoPath;
  private boolean initialized;

  /**
   * Note: use {@link GitConfigurationSourceBuilder} for building instances of this class.
   * <p>
   * Read configuration from the remote GIT repository residing at {@code repositoryURI}. Keeps a local
   * clone of the repository in the {@code tmpRepoPrefix} directory under {@code tmpPath} path.
   * Uses provided {@code branchResolver} and {@code pathResolver} for branch and path resolution.
   *
   * @param repositoryURI              URI to the remote git repository
   * @param tmpPath                    path to the tmp directory
   * @param tmpRepoPrefix              prefix for the name of the local directory keeping the repository clone
   * @param branchResolver             {@link BranchResolver} used for extracting git branch from an {@link Environment}
   * @param pathResolver               {@link PathResolver} used for extracting git path from an {@link Environment}
   * @param configFilesProvider        {@link ConfigFilesProvider} used for determining which files in repository should be read
   * @param propertiesProviderSelector selector used for choosing {@link PropertiesProvider} based on a configuration file extension
   *                                   as config files
   */
  GitConfigurationSource(String repositoryURI, Path tmpPath, String tmpRepoPrefix, BranchResolver branchResolver,
                         PathResolver pathResolver, ConfigFilesProvider configFilesProvider,
                         PropertiesProviderSelector propertiesProviderSelector) {
    this.branchResolver = requireNonNull(branchResolver);
    this.pathResolver = requireNonNull(pathResolver);
    this.configFilesProvider = requireNonNull(configFilesProvider);
    this.propertiesProviderSelector = requireNonNull(propertiesProviderSelector);
    this.repositoryURI = requireNonNull(repositoryURI);
    this.tmpPath = requireNonNull(tmpPath);
    this.tmpRepoPrefix = requireNonNull(tmpRepoPrefix);

    initialized = false;
  }

  @Override
  public Properties getConfiguration(Environment environment) {
    if (!initialized) {
      throw new IllegalStateException("Configuration source has to be successfully initialized before you request configuration.");
    }

    try {
      checkoutToBranch(branchResolver.getBranchNameFor(environment));
    } catch (GitAPIException e) {
      throw new MissingEnvironmentException(environment.getName(), e);
    }

    Properties properties = new Properties();

    List<Path> paths = new ArrayList<>();
    for (Path path : configFilesProvider.getConfigFiles()) {
      paths.add(clonedRepoPath.resolve(pathResolver.getPathFor(environment)).resolve(path));
    }

    for (Path path : paths) {
      try (InputStream input = new FileInputStream(path.toFile())) {

        PropertiesProvider provider = propertiesProviderSelector.getProvider(path.getFileName().toString());
        properties.putAll(provider.getProperties(input));

      } catch (IOException e) {
        throw new IllegalStateException("Unable to load properties from application.properties file", e);
      }
    }

    return properties;
  }

  /**
   * @throws IllegalStateException        when unable to create directories for local repo clone
   * @throws SourceCommunicationException when unable to clone repository
   */
  @Override
  public void init() {
    LOG.info("Initializing " + GitConfigurationSource.class + " pointing to " + repositoryURI);

    try {
      clonedRepoPath = Files.createTempDirectory(tmpPath, tmpRepoPrefix);
      // This folder can't exist or JGit will throw NPE on clone
      Files.delete(clonedRepoPath);
    } catch (IOException e) {
      throw new IllegalStateException("Unable to create local clone directory: " + tmpRepoPrefix, e);
    }

    try {
      clonedRepo = Git.cloneRepository()
          .setURI(repositoryURI)
          .setDirectory(clonedRepoPath.toFile())
          .call();
    } catch (GitAPIException e) {
      throw new SourceCommunicationException("Unable to clone repository: " + repositoryURI, e);
    }

    initialized = true;
  }

  @Override
  public void reload() {
    try {
      LOG.debug("Reloading configuration by pulling changes");
      clonedRepo.pull().call();
    } catch (GitAPIException e) {
      initialized = false;
      throw new IllegalStateException("Unable to pull from remote repository", e);
    }
  }

  @Override
  public void close() throws IOException {
    LOG.debug("Closing local repository: " + clonedRepoPath);
    clonedRepo.close();
    new FileUtils().deleteDir(clonedRepoPath);
  }

  private void checkoutToBranch(String branch) throws GitAPIException {
    CheckoutCommand checkoutCommand = clonedRepo.checkout()
        .setCreateBranch(false)
        .setName(branch);

    List<Ref> refList = clonedRepo.branchList().call();
    if (!anyRefMatches(refList, branch)) {
      checkoutCommand = checkoutCommand
          .setCreateBranch(true)
          .setUpstreamMode(CreateBranchCommand.SetupUpstreamMode.TRACK)
          .setStartPoint("origin/" + branch);
    }

    checkoutCommand
        .call();
  }

  private boolean anyRefMatches(List<Ref> refList, String branch) {
    for (Ref ref : refList) {
      if (ref.getName().replace("refs/heads/", "").equals(branch)) {
        return true;
      }
    }

    return false;
  }

  @Override
  public String toString() {
    return "GitConfigurationSource{" +
        "clonedRepo=" + clonedRepo +
        ", clonedRepoPath=" + clonedRepoPath +
        ", branchResolver=" + branchResolver +
        ", pathResolver=" + pathResolver +
        ", configFilesProvider=" + configFilesProvider +
        '}';
  }
}

@@ -5468,46 +5468,50 @@
 oad 
-properties from application.properties
+configuration from %22 + path.toString() + %22
  fil

improve exception message when unable to load file in GitConfigurationSource - fixes #159