/*
*  Copyright (c) 2005-2013, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
*  WSO2 Inc. 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.wso2.carbon.deployment.synchronizer.git;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.eclipse.jgit.api.*;
import org.eclipse.jgit.api.errors.*;
import org.eclipse.jgit.storage.file.FileRepository;
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
import org.wso2.carbon.deployment.synchronizer.ArtifactRepository;
import org.wso2.carbon.deployment.synchronizer.DeploymentSynchronizerException;
import org.wso2.carbon.deployment.synchronizer.RepositoryManager;
import org.wso2.carbon.deployment.synchronizer.git.internal.AbstractBehaviour;
import org.wso2.carbon.deployment.synchronizer.git.internal.GitDeploymentSynchronizerConstants;
import org.wso2.carbon.deployment.synchronizer.git.internal.GitDeploymentSyncronizerConfiguration;
import org.wso2.carbon.deployment.synchronizer.git.repository_creator.GitBlitBasedRepositoryCreator;
import org.wso2.carbon.deployment.synchronizer.git.repository_creator.SCMBasedRepositoryCreator;
import org.wso2.carbon.deployment.synchronizer.git.stratos2.S2Behaviour;
import org.wso2.carbon.deployment.synchronizer.git.stratos2.S2GitRepositoryManager;
import org.wso2.carbon.deployment.synchronizer.git.util.CarbonUtilities;
import org.wso2.carbon.deployment.synchronizer.git.util.FileUtilities;
import org.wso2.carbon.deployment.synchronizer.git.util.GitUtilities;
import org.wso2.carbon.deployment.synchronizer.DeploymentSynchronizerConstants;
import org.wso2.carbon.deployment.synchronizer.util.RepositoryConfigParameter;
import org.wso2.carbon.utils.multitenancy.MultitenantUtils;

import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Set;

/**
 * Git based artifact repository
 */

public class GitBasedArtifactRepository implements ArtifactRepository {

    private static final Log log = LogFactory.getLog(GitBasedArtifactRepository.class);

    private RepositoryManager repositoryManager;
    private GitDeploymentSyncronizerConfiguration gitDepsyncConfig;
    private AbstractBehaviour behaviour;

    /*
    * Constructor
    * */
    public GitBasedArtifactRepository () {

        //TODO:fix properly
        if(!isGitDeploymentSyncEnabled()) {
            return;
        }

        readConfiguration();

        //standard worker manager separated deployment
        if(gitDepsyncConfig.isStandardDeployment()) {
            //GitBlit git server
            if(gitDepsyncConfig.getGitServer().equals(GitDeploymentSynchronizerConstants.SERVER_GITBLIT)){
                repositoryManager = new DefaultGitRepositoryManager(new GitBlitBasedRepositoryCreator());
            }
            //SCM git server
            else if (gitDepsyncConfig.getGitServer().equals(GitDeploymentSynchronizerConstants.SERVER_SCM)) {
                repositoryManager = new DefaultGitRepositoryManager(new SCMBasedRepositoryCreator());
            }
            //No specific Git server specified - use a single repository
            else if (gitDepsyncConfig.getGitServer().equals(GitDeploymentSynchronizerConstants.SERVER_UNSPECIFIED)) {
                repositoryManager = new SingleTenantGitRepositoryManager();
            }
            behaviour = new DefaultBehaviour();
        }
        //Stratos 2 specific deployment
        else {
            repositoryManager = new S2GitRepositoryManager();
            behaviour = new S2Behaviour();
        }
    }

    private boolean isGitDeploymentSyncEnabled () {

        //check if deployment synchronization is enabled
        String depSyncEnabledParam = CarbonUtilities.readConfigurationParameter(GitDeploymentSynchronizerConstants.ENABLED);

        //Check if deployment synchronization is enabled
        if (depSyncEnabledParam != null && depSyncEnabledParam.equals("true")) {

            //check if repository type is 'git', else no need to create GitBasedArtifactRepository instance
            String repoTypeParam = CarbonUtilities.readConfigurationParameter(GitDeploymentSynchronizerConstants.REPOSITORY_TYPE);
            if (repoTypeParam != null && repoTypeParam.equals(DeploymentSynchronizerConstants.REPOSITORY_TYPE_GIT)) {
                 return true;
            }
        }

        return false;
    }

    /**
     * Reads the configuration
     */
    private void readConfiguration () {

        gitDepsyncConfig = new GitDeploymentSyncronizerConfiguration();

        String standardDeploymentParam = CarbonUtilities.readConfigurationParameter(GitDeploymentSynchronizerConstants.DEPLOYMENT_METHOD);
        if (standardDeploymentParam != null && (standardDeploymentParam.equalsIgnoreCase("true") || standardDeploymentParam.equalsIgnoreCase("false"))) {
            gitDepsyncConfig.setStandardDeployment(Boolean.parseBoolean(standardDeploymentParam));
        }

        String gitServerParam = CarbonUtilities.readConfigurationParameter(GitDeploymentSynchronizerConstants.GIT_SERVER);
        if (gitServerParam != null) {
            gitDepsyncConfig.setGitServer(gitServerParam);
        }
    }

    /**
     * Called at tenant load to do initialization related to the tenant
     *
     * @param tenantId id of the tenant
     * @throws DeploymentSynchronizerException in case of an error
     */
    public void init (int tenantId) throws DeploymentSynchronizerException {

        TenantGitRepositoryContext repoCtx = new TenantGitRepositoryContext();

        String gitLocalRepoPath = MultitenantUtils.getAxis2RepositoryPath(tenantId);
        repoCtx.setTenantId(tenantId);
        repoCtx.setLocalRepoPath(gitLocalRepoPath);

        FileRepository localRepo = null;
        try {
            localRepo = new FileRepository(new File(gitLocalRepoPath + "/.git"));

        } catch (IOException e) {
            handleError("Error creating git local repository for tenant " + tenantId, e);
        }

        repoCtx.setLocalRepo(localRepo);
        repoCtx.setGit(new Git(localRepo));
        repoCtx.setCloneExists(false);

        TenantGitRepositoryContextCache.getTenantRepositoryContextCache().cacheTenantGitRepoContext(tenantId, repoCtx);

        //provision a repository
        repositoryManager.provisionRepository(tenantId);
        //repositoryManager.addRepository(tenantId, url);

        repositoryManager.getUrlInformation(tenantId);
        repositoryManager.getCredentialsInformation(tenantId);
    }

    /**
     * Commits any changes in the local repository to the relevant remote repository
     *
     * @param localRepoPath tenant's local repository path
     * @return true if commit is successful, else false
     * @throws DeploymentSynchronizerException in case of an error
     */
    public boolean commit(int tenantId, String localRepoPath) throws DeploymentSynchronizerException {

        String gitRepoUrl = repositoryManager.getUrlInformation(tenantId).getUrl();
        if(gitRepoUrl == null) { //url not available
            log.warn ("Remote repository URL not available for tenant " + tenantId + ", aborting commit");
            return false;
        }

        TenantGitRepositoryContext gitRepoCtx = TenantGitRepositoryContextCache.getTenantRepositoryContextCache().
                retrieveCachedTenantGitContext(tenantId);


        Status status = getGitStatus(gitRepoCtx);
        if (status == null) {
            return false;
        }

        if(status.isClean()) {//no changes, nothing to commit
            if(log.isDebugEnabled())
                log.debug("No changes detected in the local repository at " + localRepoPath);
            return false;
        }

        if(!addArtifacts(gitRepoCtx, getNewArtifacts(status)) &&
                !addArtifacts(gitRepoCtx, getModifiedArtifacts(status)) &&
                !removeArtifacts(gitRepoCtx, getRemovedArtifacts(status))) { //no changes!

            return false;
        }

        commitToLocalRepo(gitRepoCtx);
        pushToRemoteRepo(gitRepoCtx);

        return behaviour.requireSynchronizeRepositoryRequest();
    }


    /**
     * Quesries the git status for the repository given by gitRepoCtx
     *
     * @param gitRepoCtx TenantGitRepositoryContext instance for the tenant
     * @return Status instance updated with relevant status information,
     *         null in an error in getting the status
     */
    private Status getGitStatus (TenantGitRepositoryContext gitRepoCtx) {

        Git git = gitRepoCtx.getGit();
        StatusCommand statusCmd = git.status();
        Status status;

        try {
            status = statusCmd.call();

        } catch (GitAPIException e) {
            log.error("Git status operation for tenant " + gitRepoCtx.getTenantId() + " failed, ", e);
            status = null;
        }

        return status;
    }

    /**
     * Returns the newly added artifact set relevant to the current status of the repository
     *
     * @param status git status
     * @return artifact names set
     */
    private Set<String> getNewArtifacts (Status status) {

        return status.getUntracked();
    }

    /**
     * Returns the removed (undeployed) artifact set relevant to the current status of the repository
     *
     * @param status git status
     * @return artifact names set
     */
    private Set<String> getRemovedArtifacts (Status status) {

        return status.getMissing();
    }

    /**
     * Return the modified artifacts set relevant to the current status of the repository
     *
     * @param status git status
     * @return artifact names set
     */
    private Set<String> getModifiedArtifacts (Status status) {

        return status.getModified();
    }

    /**
     * Adds the artifacts to the local staging area
     *
     * @param gitRepoCtx TenantGitRepositoryContext instance
     * @param artifacts set of artifacts
     * @return true if artifacts were added
     */
    private boolean addArtifacts (TenantGitRepositoryContext gitRepoCtx, Set<String> artifacts) {

        if(artifacts.isEmpty()) {
            return false;
        }

        boolean artifactsAdded;
        AddCommand addCmd = gitRepoCtx.getGit().add();
        for (String artifact : artifacts) {
            addCmd.addFilepattern(artifact);
        }

        try {
            addCmd.call();
            artifactsAdded = true;

        } catch (GitAPIException e) {
            log.error("Adding artifact to the repository at " + gitRepoCtx.getLocalRepoPath() + "failed", e);
            artifactsAdded = false;
        }

        return artifactsAdded;
    }

    /**
     * Removes the set of artifacts from local repo
     *
     * @param gitRepoCtx TenantGitRepositoryContext instance
     * @param artifacts Set of artifact names to remove
     * @return true if artifacts were removed
     */
    private boolean removeArtifacts (TenantGitRepositoryContext gitRepoCtx, Set<String> artifacts) {

        if(artifacts.isEmpty()) {
            return false;
        }

        boolean artifactsRemoved;
        RmCommand rmCmd = gitRepoCtx.getGit().rm();
        for (String artifact : artifacts) {
            rmCmd.addFilepattern(artifact);
        }

        try {
            rmCmd.call();
            artifactsRemoved = true;

        } catch (GitAPIException e) {
            log.error("Removing artifact from the repository at " + gitRepoCtx.getLocalRepoPath() + "failed", e);
            artifactsRemoved = false;
        }

        return artifactsRemoved;
    }

    /**
     * Commits changes for a tenant to relevant the local repository
     *
     * @param gitRepoCtx TenantGitRepositoryContext instance for the tenant
     */
    private void commitToLocalRepo (TenantGitRepositoryContext gitRepoCtx) {

        CommitCommand commitCmd = gitRepoCtx.getGit().commit();
        commitCmd.setMessage("tenant " + gitRepoCtx.getTenantId() + "'s artifacts committed to repository at " +
                gitRepoCtx.getLocalRepoPath() + ", time stamp: " + System.currentTimeMillis());

        try {
            commitCmd.call();

        } catch (GitAPIException e) {
            log.error("Committing artifacts to repository failed for tenant " + gitRepoCtx.getTenantId(), e);
        }
    }

    /**
     * Pushes the artifacts of the tenant to relevant remote repository
     *
     * @param gitRepoCtx TenantGitRepositoryContext instance for the tenant
     */
    private void pushToRemoteRepo(TenantGitRepositoryContext gitRepoCtx) {

        PushCommand pushCmd = gitRepoCtx.getGit().push();

        UsernamePasswordCredentialsProvider credentialsProvider = GitUtilities.createCredentialsProvider(repositoryManager,
                gitRepoCtx.getTenantId());

        if (credentialsProvider == null) {
            log.warn ("Remote repository credentials not available for tenant " + gitRepoCtx.getTenantId() +
                    ", aborting push");
            return;
        }
        pushCmd.setCredentialsProvider(credentialsProvider);

        try {
            pushCmd.call();

        } catch (GitAPIException e) {
            log.error("Pushing artifacts to remote repository failed for tenant " + gitRepoCtx.getTenantId(), e);
        }
    }

    /**
     * Method inherited from ArtifactRepository for initializing checkout
     *
     * @param localRepoPath local repository path of the tenant
     * @return true if success, else false
     * @throws DeploymentSynchronizerException if an error occurs
     */
    public boolean checkout (int tenantId, String localRepoPath) throws DeploymentSynchronizerException {

        String gitRepoUrl = repositoryManager.getUrlInformation(tenantId).getUrl();
        if(gitRepoUrl == null) { //url not available
            log.warn ("Remote repository URL not available for tenant " + tenantId + ", aborting checkout");
            return false;
        }

        TenantGitRepositoryContext gitRepoCtx = TenantGitRepositoryContextCache.getTenantRepositoryContextCache().
                retrieveCachedTenantGitContext(tenantId);

        File gitRepoDir = new File(gitRepoCtx.getLocalRepoPath());
        if (!gitRepoDir.exists()) {
             return cloneRepository(gitRepoCtx);
        }
        else {
            if (GitUtilities.isValidGitRepo(gitRepoCtx.getLocalRepo())) {
                log.info("Existing git repository detected for tenant " + gitRepoCtx.getTenantId() +
                        ", no clone required");
                try {
                    return pullArtifacts(gitRepoCtx);

                } catch (CheckoutConflictException e) {
                    //conflict(s) detected, try to checkout from local index
                    if(checkoutFromLocalIndex(gitRepoCtx, e.getConflictingPaths())) {
                        try {
                            //now pull the changes from remote repository
                            return pullArtifacts(gitRepoCtx);

                        } catch (CheckoutConflictException e1) {
                            //cannot happen here
                            log.error("Git pull for the path " + e1.getConflictingPaths().toString() +
                                    " failed due to conflicts", e1);
                        }
                    }
                    return false;
                }
            }
            else {
                if (behaviour.requireInitialLocalArtifactSync()) {
                    return syncInitialLocalArtifacts(gitRepoCtx);
                }
                else {
                    if(log.isDebugEnabled()) {
                        log.debug("Repository for tenant " + gitRepoCtx.getTenantId() + " is not a valid git repo, will try to delete");
                    }
                    FileUtilities.deleteFolderStructure(gitRepoDir);
                    return cloneRepository(gitRepoCtx);
                }
            }
        }

        /*if(behaviour.requireInitialLocalArtifactSync() && !gitRepoCtx.initialArtifactsSynced()) {
            return syncInitialLocalArtifacts(gitRepoCtx);
        }
        else if(!gitRepoCtx.cloneExists()) {
            return cloneRepository(gitRepoCtx);
        }
        else {
            return pullArtifacts(gitRepoCtx);
        }*/
    }

    /**
     * Sync any local artifact that are initially available with a remote repository
     *
     * @param gitRepoCtx TenantGitRepositoryContext instance
     * @return true if sync is success, else false
     */
    private boolean syncInitialLocalArtifacts(TenantGitRepositoryContext gitRepoCtx) {

        boolean syncedLocalArtifacts = false;

        Status status = getGitStatus(gitRepoCtx);
        if(status != null && !status.isClean()) {
            //initialize repository
            GitUtilities.InitGitRepository(new File(gitRepoCtx.getLocalRepoPath()));
            //add the remote repository (origin)
            syncedLocalArtifacts = GitUtilities.addRemote(gitRepoCtx.getLocalRepo(),
                    gitRepoCtx.getRemoteRepoUrl());
        }

        return syncedLocalArtifacts;
    }

    /**
     * Clones the remote repository to the local repository path
     *
     * @param gitRepoCtx TenantGitRepositoryContext for the tenant
     * @return true if clone is success, else false
     */
    private boolean cloneRepository (TenantGitRepositoryContext gitRepoCtx) { //should happen only at the beginning

        boolean cloneSuccess = false;

        File gitRepoDir = new File(gitRepoCtx.getLocalRepoPath());
        /*if (gitRepoDir.exists()) {
            if(GitUtilities.isValidGitRepo(gitRepoCtx.getLocalRepo())) { //check if a this is a valid git repo
                log.info("Existing git repository detected for tenant " + gitRepoCtx.getTenantId() +
                        ", no clone required");
                gitRepoCtx.setCloneExists(true);
                return true;
            }
            else {
                if(log.isDebugEnabled()) {
                    log.debug("Repository for tenant " + gitRepoCtx.getTenantId() + " is not a valid git repo, will try to delete");
                }
                FileUtilities.deleteFolderStructure(gitRepoDir); //if not a valid git repo but non-empty, delete it (else the clone will not work)
            }
        }*/

        CloneCommand cloneCmd =  Git.cloneRepository().
                setURI(gitRepoCtx.getRemoteRepoUrl()).
                setDirectory(gitRepoDir).
                setBranch(GitDeploymentSynchronizerConstants.GIT_REFS_HEADS_MASTER);

        UsernamePasswordCredentialsProvider credentialsProvider = GitUtilities.createCredentialsProvider(repositoryManager,
                gitRepoCtx.getTenantId());

        if (credentialsProvider == null) {
            log.warn ("Remote repository credentials not available for tenant " + gitRepoCtx.getTenantId() +
                    ", aborting clone");
            return false;
        }
        cloneCmd.setCredentialsProvider(credentialsProvider);

        try {
            cloneCmd.call();
            log.info("Git clone operation for tenant " + gitRepoCtx.getTenantId() + " successful");
            gitRepoCtx.setCloneExists(true);
            cloneSuccess = true;

        } catch (TransportException e) {
            log.error("Accessing remote git repository failed for tenant " + gitRepoCtx.getTenantId(), e);

        } catch (GitAPIException e) {
            log.error("Git clone operation for tenant " + gitRepoCtx.getTenantId() + " failed", e);
        }

        return cloneSuccess;
    }

    /**
     * Pulling if any updates are available in the remote git repository. If basic authentication is required,
     * will call 'RepositoryInformationService' for credentials.
     *
     * @param gitRepoCtx TenantGitRepositoryContext instance for tenant
     * @return true if success, else false
     */
    private boolean pullArtifacts (TenantGitRepositoryContext gitRepoCtx) throws CheckoutConflictException {

        PullCommand pullCmd = gitRepoCtx.getGit().pull();

        UsernamePasswordCredentialsProvider credentialsProvider = GitUtilities.createCredentialsProvider(repositoryManager,
                gitRepoCtx.getTenantId());

        if (credentialsProvider == null) {
            log.warn ("Remote repository credentials not available for tenant " + gitRepoCtx.getTenantId() +
                    ", aborting pull");
            return false;
        }
        pullCmd.setCredentialsProvider(credentialsProvider);

        try {
            pullCmd.call();

        } catch (InvalidConfigurationException e) {
            log.warn("Git pull unsuccessful for tenant " + gitRepoCtx.getTenantId() + ", " + e.getMessage());
            FileUtilities.deleteFolderStructure(new File(gitRepoCtx.getLocalRepoPath()));
            cloneRepository(gitRepoCtx);
            return true;

        } catch (JGitInternalException e) {
            log.warn("Git pull unsuccessful for tenant " + gitRepoCtx.getTenantId() + ", " + e.getMessage());
            return false;

        } catch (TransportException e) {
            log.error("Accessing remote git repository " + gitRepoCtx.getRemoteRepoUrl() + " failed for tenant " + gitRepoCtx.getTenantId(), e);
            return false;

        } catch (CheckoutConflictException e) {
            log.warn("Git pull for the path " + e.getConflictingPaths().toString() + " failed due to conflicts");
            //FileUtilities.deleteFolderStructure(new File(gitRepoCtx.getLocalRepoPath()));
            //cloneRepository(gitRepoCtx);
            throw e;

        } catch (GitAPIException e) {
            log.error("Git pull operation for tenant " + gitRepoCtx.getTenantId() + " failed", e);
            return false;
        }

        return true;
    }

    /**
     * Checkout the artifacts from the local index. Any local working copy changes will be discarded.
     *
     * @param gitRepoCtx TenantGitRepositoryContext instance for the current tenant
     * @param paths List of paths for artifacts with conflicts
     * @return true if successfully checked out all the files from local index, else false
     */
    private boolean checkoutFromLocalIndex(TenantGitRepositoryContext gitRepoCtx, List<String> paths) {

        boolean checkoutSuccess = false;

        if(paths.isEmpty()) {
            return checkoutSuccess;
        }

        CheckoutCommand checkoutCmd = gitRepoCtx.getGit().checkout();
        for(String path : paths) {
            checkoutCmd.addPath(path);
        }

        try {
            checkoutCmd.call();
            if(log.isDebugEnabled()) {
                for(String path : paths) {
                    log.debug("Checked out conflicting file " + path + " from the local index successfully");
                }
            }
            checkoutSuccess = true;
            log.info("Checked out the conflicting files from the local index successfully");

        } catch (GitAPIException e) {
            log.error("Checking out artifacts from index failed", e);
        }

        return checkoutSuccess;
    }

    public void initAutoCheckout(boolean b) throws DeploymentSynchronizerException {
        //no implementation
    }

    public void cleanupAutoCheckout() {
        //no implementation
    }

    /**
     * Return the repository type
     *
     * @return repository type, i.e. git
     */
    public String getRepositoryType() {

        return DeploymentSynchronizerConstants.REPOSITORY_TYPE_GIT;
    }

    public List<RepositoryConfigParameter> getParameters() {

        return null;
    }

    /**
     * Partial checkout with defined depth. Currently not supported in GIT.
     *
     * @param tenantId tenant id
     * @param filePath local repository path
     * @param depth depth to checkout (0 - 3)
     * @return if success true, else false
     * @throws DeploymentSynchronizerException if an error occurs
     */
    public boolean checkout(int tenantId, String filePath, int depth) throws DeploymentSynchronizerException {

        return checkout(tenantId, filePath); //normal checkout is done
    }

    /**
     * Partial update with defined depth.Currently not supported in GIT.
     *
     * @param tenantId tenant Id
     * @param rootPath root path to the local repository
     * @param filePath path to sub directory to update
     * @param depth depth to update (0 - 3)
     * @return if success true, else false
     * @throws DeploymentSynchronizerException if an error occurs
     */
    public boolean update(int tenantId, String rootPath, String filePath, int depth) throws DeploymentSynchronizerException {

        return checkout(tenantId, rootPath); //normal checkout is done
    }

    /**
     * removed tenant's information from the cache
     *
     * @param tenantId tenant Id
     */
    public void cleanupTenantContext(int tenantId) {
        TenantGitRepositoryContextCache.getTenantRepositoryContextCache().
                removeCachedTenantGitContext(tenantId);
    }

    private void handleError (String errorMsg, Exception e) throws DeploymentSynchronizerException {
        log.error(errorMsg, e);
        throw new DeploymentSynchronizerException(errorMsg, e);
    }

}
