/*
 * Copyright 2020 ETSI
 * 
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions are met:
 * 1. Redistributions of source code must retain the above copyright notice, 
 *    this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice, 
 *    this list of conditions and the following disclaimer in the documentation 
 *    and/or other materials provided with the distribution.
 * 3. Neither the name of the copyright holder nor the names of its contributors 
 *    may be used to endorse or promote products derived from this software without 
 *    specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
 * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package fr.mines_stetienne.ci.saref.managers;

import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import org.apache.commons.io.FileUtils;
import org.apache.commons.text.StringSubstitutor;
import org.apache.jena.rdf.model.Resource;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;

import fr.mines_stetienne.ci.saref.SAREF;
import fr.mines_stetienne.ci.saref.SAREFErrorLogger;
import fr.mines_stetienne.ci.saref.SAREFPipeline;
import fr.mines_stetienne.ci.saref.SAREFPipelineException;
import fr.mines_stetienne.ci.saref.SAREFPipeline.Mode;
import fr.mines_stetienne.ci.saref.checkers.Clause_9_Checker;
import fr.mines_stetienne.ci.saref.checkers.Clause_10_Checker;
import fr.mines_stetienne.ci.saref.checkers.TermsChecker;
import fr.mines_stetienne.ci.saref.entities.SAREFProject;
import fr.mines_stetienne.ci.saref.entities.SAREFRepository;
import fr.mines_stetienne.ci.saref.entities.SAREFVersion;
import fr.mines_stetienne.ci.saref.entities.SAREFVersionName;

/**
 *
 * @author Maxime Lefrançois
 *
 */
public class SourcesManager extends SAREFErrorLogger {

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

	private static final String CONFIGURATION_FILE_NAME = ".saref-repositories.yml";

	private static enum MESSAGE {
		versions, yaml_error, yaml_repository, yaml_fetch, yaml_failure, reset_checkout_error, check_versions_error,
		checkout_version, check_terms_error, no_repository_manager;
	}

	private final File sourcesDir;
	private final Map<SAREFProject, RepositoryManager> sourcesManagers = new HashMap<>();
	private RepositoryManager targetRepositoryManager;
	private final Map<SAREFProject,Set<Resource>> noRepositoryManager = new HashMap<>();

	public SourcesManager(SAREFPipeline pipeline, Logger errorLogger) {
		super(pipeline, errorLogger);
		this.sourcesDir = new File(pipeline.targetDir, SAREF.NAME_SOURCES);
	}
	
	public void initTargetRepositoryManager() throws SAREFPipelineException {
		if (pipeline.mode == Mode.PRERELEASE_PORTAL || pipeline.mode == Mode.RELEASE_PORTAL) {
			targetRepositoryManager = null;
		} else {
			File directory = pipeline.directory;
			Logger repositoryLogger = pipeline.getLogger(getMessage(MESSAGE.versions, directory.getName()));
			RepositoryFactory repositoryFactory = new RepositoryFactory(this, repositoryLogger, true);
			SAREFRepository repository = repositoryFactory.create(directory);
			targetRepositoryManager = new RepositoryManager(pipeline, repositoryLogger, repository, true);
		}
	}

	/**
	 * Fetch all the repositories in the configuration file.
	 * 
	 * @throws SAREFPipelineException
	 */
	public void fetchRepositories() throws SAREFPipelineException {
		final File confFile = new File(pipeline.directory, CONFIGURATION_FILE_NAME);
		if (!confFile.exists()) {
			if (pipeline.mode == Mode.PRERELEASE_PORTAL || pipeline.mode == Mode.RELEASE_PORTAL) {
				logError(getMessage(MESSAGE.yaml_error, CONFIGURATION_FILE_NAME));
			}
			return;
		}
		YAMLRepos repos;
		try {
			ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
			repos = mapper.readValue(confFile, YAMLRepos.class);
		} catch (IOException ex) {
			logError(getMessage(MESSAGE.yaml_error, CONFIGURATION_FILE_NAME));
			return;
		}
		try {
			FileUtils.forceMkdir(sourcesDir);
		} catch (IOException ex) {
			logError(getMessage(MESSAGE.yaml_repository, sourcesDir), ex);
			return;
		}
		for (YAMLRepos.Entry<String, YAMLHost> entry : repos.entrySet()) {
			final String hostName = entry.getKey();
			final YAMLHost host = entry.getValue();
			fetchRepository(hostName, host);
		}
	}

	/**
	 * Fetches a repository and adds its manager to the map.
	 * 
	 * @param hostName
	 * @param host
	 * @throws SAREFPipelineException
	 */
	private void fetchRepository(String hostName, YAMLHost host) throws SAREFPipelineException {
		final CredentialsProvider credentialsProvider = getCredentialsProvider(hostName, host.credentials);
		for (String repo : host.repos) {
			final String name = repo.substring(repo.lastIndexOf("/") + 1);
			File repositoryDirectory = new File(sourcesDir, name);

			Logger repositoryLogger = pipeline.getLogger(getMessage(MESSAGE.versions, name));
			LOG.info("Fetch repository " + name);
			if (repositoryDirectory.isDirectory()) {
				try (Git git = Git.open(repositoryDirectory)) {
					git.fetch().setCredentialsProvider(credentialsProvider).setRemoveDeletedRefs(true).call();
				} catch (Exception ex) {
					logError(getMessage(MESSAGE.yaml_fetch, name), ex);
					continue;
				}
			} else {
				String http_url_to_repo = String.format("https://%s/%s.git", hostName, repo);
				try (Git git = Git.cloneRepository().setCredentialsProvider(credentialsProvider)
						.setURI(http_url_to_repo).setDirectory(repositoryDirectory).call()) {
				} catch (Exception ex) {
					logError(getMessage(MESSAGE.yaml_failure, http_url_to_repo, name), ex);
					continue;
				}
			}
			RepositoryFactory repositoryFactory = new RepositoryFactory(this, repositoryLogger, false);
			SAREFRepository repository = repositoryFactory.create(repositoryDirectory);
			if (repository == null) {
				continue;
			}
			SAREFProject project = repository.getProject();
			RepositoryManager repositoryManager = new RepositoryManager(pipeline, repositoryLogger, repository, false);
			sourcesManagers.put(project, repositoryManager);
		}
	}

	private CredentialsProvider getCredentialsProvider(String hostName, YAMLCredentials credentials) {
		final String username;
		final char[] password;
		if (credentials != null && credentials.username != null) {
			username = StringSubstitutor.replace(credentials.username, System.getenv());
		} else {
			username = pipeline.credentialsProvider.getUsername(hostName);
		}
		if (credentials != null && credentials.password != null) {
			password = StringSubstitutor.replace(credentials.password, System.getenv()).toCharArray();
		} else {
			password = pipeline.credentialsProvider.getPassword(hostName);
		}
		return new UsernamePasswordCredentialsProvider(username, password);
	}

	private static class YAMLRepos extends HashMap<String, YAMLHost> {
		private static final long serialVersionUID = 3434677850580166200L;
	}

	private static class YAMLHost {
		private YAMLCredentials credentials;
		private List<String> repos;

		@JsonCreator
		public YAMLHost(@JsonProperty(value = "credentials", required = false) YAMLCredentials credentials,
				@JsonProperty(value = "repos", required = false) List<String> repos) {
			this.credentials = credentials;
			this.repos = repos;
		}
	}

	private static class YAMLCredentials {
		private String username;
		private String password;

		@JsonCreator
		public YAMLCredentials(@JsonProperty(value = "username", required = false) String username,
				@JsonProperty(value = "password", required = false) String password) {
			this.username = username;
			this.password = password;
		}
	}

	public boolean isTargetRepositoryManager(RepositoryManager repositoryManager) {
		if (targetRepositoryManager == null) {
			return false;
		}
		return targetRepositoryManager.equals(repositoryManager);
	}

	public RepositoryManager getTargetRepositoryManager() {
		return targetRepositoryManager;
	}

	public Collection<RepositoryManager> getSourceRepositoryManagers() {
		return sourcesManagers.values();
	}


	/**
	 * Load the ontology and examples in the repositories.
	 * 
	 * @throws SAREFPipelineException
	 */
	public void loadRepositories() throws SAREFPipelineException {
		if (targetRepositoryManager != null) {
			targetRepositoryManager.loadRepository();
		}
		for (RepositoryManager repositoryManager : sourcesManagers.values()) {
			repositoryManager.loadRepository();
		}
	}
	
	public void checkClauses() throws SAREFPipelineException {
		if (targetRepositoryManager != null) {
			// Mode is DEVELOP or RELEASE.
			new Clause_9_Checker(targetRepositoryManager).check();
			new Clause_10_Checker(targetRepositoryManager).check();
		} else {
			// Mode is PORTAL. Check every source
			boolean hasErrors = false;
			for (RepositoryManager repositoryManager : sourcesManagers.values()) {
				SAREFRepository repository = repositoryManager.getRepository();
				for (SAREFVersionName versionName : repository.getVersions().keySet()) {
					if (versionName.equals(SAREFVersionName.DEFAULT)) {
						continue;
					}
					try {
						repositoryManager.checkoutVersion(versionName);
						new Clause_9_Checker(repositoryManager).check();
						new Clause_10_Checker(repositoryManager).check();
					} catch (SAREFPipelineException ex) {
						hasErrors = true;
					}
				}
			}
			if (hasErrors) {
				logError(getMessage(MESSAGE.check_versions_error));
			}
		}
	}

	public void checkTerms() throws SAREFPipelineException {
		if (!noRepositoryManager.isEmpty()) {
			for(SAREFProject project: noRepositoryManager.keySet()) {
				logError(getMessage(MESSAGE.no_repository_manager,
						project, project,
						noRepositoryManager.get(project).stream().map(Object::toString).collect(Collectors.joining(", "))));
			}
		}
		if (targetRepositoryManager != null) {
			// Mode is DEVELOP or RELEASE.
			new TermsChecker(pipeline, targetRepositoryManager.getRepository()).check();
		} else {
			// Mode is PORTAL. Check every source
			boolean hasErrors = false;
			for (RepositoryManager repositoryManager : sourcesManagers.values()) {
				try {
					new TermsChecker(pipeline, repositoryManager.getRepository()).check();
				} catch (SAREFPipelineException ex) {
					hasErrors = true;
				}
			}
			if (hasErrors) {
				logError(getMessage(MESSAGE.check_terms_error));
			}
		}
	}

	public void generateSite() throws SAREFPipelineException {
		if (pipeline.ignoreSite) {
			return;
		}

		if (targetRepositoryManager != null) {
			// Mode is DEVELOP or RELEASE.
			SAREFRepository repository = targetRepositoryManager.getRepository();
			VersionSiteManager versionSiteManager = new VersionSiteManager(targetRepositoryManager);
			versionSiteManager.generateSite();
			if (!pipeline.ignoreTerms) {
				new TermSiteManager(pipeline, repository).generateSite();
			}
			siteManager.generateHtaccess();
		} else {
			// Mode is PORTAL. generate for every source
			for (RepositoryManager repositoryManager : sourcesManagers.values()) {
				SAREFRepository repository = repositoryManager.getRepository();
				for (SAREFVersionName versionName : repositoryManager.getRepository().getVersions().keySet()) {
					if (versionName.equals(SAREFVersionName.DEFAULT)) {
						continue;
					}
					try {
						repositoryManager.checkoutVersion(versionName);
						VersionSiteManager versionSiteManager = new VersionSiteManager(repositoryManager);
						versionSiteManager.generateSite();
					} catch (SAREFPipelineException ex) {
						String project = repositoryManager.getRepository().getProject().getName();
						String msg = getMessage(MESSAGE.checkout_version, project, versionName);
						errorLogger.error(msg);
					}
				}
				if (!pipeline.ignoreTerms) {
					new TermSiteManager(pipeline, repository).generateSite();
				}
			}
			siteManager.generateHtaccess();
		}
	}

	public void resetCheckout() throws SAREFPipelineException {
		boolean hasErrors = false;
//		if(targetRepositoryManager != null) {
//			try {
//				targetRepositoryManager.resetCheckout();
//			} catch (SAREFPipelineException ex) {
//				hasErrors = true;
//			}
//		}
		for (RepositoryManager repositoryManager : sourcesManagers.values()) {
			try {
				repositoryManager.resetCheckout();
			} catch (SAREFPipelineException ex) {
				hasErrors = true;
			}
		}
		if (hasErrors) {
			logWarning(getMessage(MESSAGE.reset_checkout_error));
		}
	}


	public RepositoryManager findRepositoryManager(String uri) {
		SAREFProject project = SAREF.extractProject(uri);
		if (project == null) {
			return null;
		}
		return findRepositoryManager(project);
	}

	public SAREFVersion findVersion(String uri) {
		RepositoryManager repositoryManager = findRepositoryManager(uri);
		if (repositoryManager == null) {
			return null;
		}
		SAREFVersionName versionName = SAREF.extractVersionName(uri);
		if (versionName == null) {
			return null;
		}
		return repositoryManager.getRepository().getVersion(versionName);
	}

	public RepositoryManager findRepositoryManager(SAREFProject project) {
		if (targetRepositoryManager != null && targetRepositoryManager.getRepository().getProject().equals(project)) {
			return targetRepositoryManager;
		}
		return sourcesManagers.get(project);
	}
	
	public Ref getRef(SAREFVersion version) {
		if(version == null) {
			return null;
		}
		if(version.getReleaseRef() != null) {
			return version.getReleaseRef();
		}
		if(pipeline.mode == Mode.RELEASE_PORTAL) {
			return null;
		}
		if(version.getPrereleaseRef() != null) {
			return version.getPrereleaseRef();
		}
		if(pipeline.mode == Mode.PRERELEASE_PORTAL) {
			return null;
		}
		return version.getDevelopRef();
	}

	public void addNoRepositoryManager(Resource t) {
		String iri = t.getURI();
		SAREFProject project = SAREF.extractProject(iri);
		Set<Resource> terms = noRepositoryManager.get(project);
		if (terms == null) {
			terms = new HashSet<>();
			noRepositoryManager.put(project, terms);
		}
		terms.add(t);		
	}

}
