# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF 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.

import shutil
import subprocess
import tempfile
import urllib
from distutils.dir_util import copy_tree
from threading import current_thread

import os
import time
from config import Config
from exception import GitRepositorySynchronizationException
from git import *

from ...util.asyncscheduledtask import AbstractAsyncScheduledTask, ScheduledExecutor
from ...util.log import LogFactory


class AgentGitHandler:
    """
    Handles all the git artifact management tasks related to a cartridge
    """

    def __init__(self):
        pass

    log = LogFactory().get_log(__name__)

    __git_repositories = {}

    # (tenant_id => GitRepository)

    @staticmethod
    def sync_initial_local_artifacts(git_repo):
        # init git repo
        AgentGitHandler.init(git_repo.local_repo_path)

        # add remote repos
        return AgentGitHandler.add_remote(git_repo)

    @staticmethod
    def add_remote(git_repo):
        # add origin remote
        output, errors = AgentGitHandler.execute_git_command(["remote", "add", "origin", git_repo.repo_url],
                                                             git_repo.local_repo_path)
        if len(output) > 0:
            raise GitRepositorySynchronizationException("Error in adding remote origin %s for local repository %s"
                                                        % (git_repo.repo_url, git_repo.local_repo_path))

        # fetch
        output, errors = AgentGitHandler.execute_git_command(["fetch"], git_repo.local_repo_path)
        if "Resolving deltas: 100%" not in output:
            raise GitRepositorySynchronizationException(
                "Error in fetching from remote origin %s for local repository %s"
                % (git_repo.repo_url, git_repo.local_repo_path))

        # checkout master
        output, errors = AgentGitHandler.execute_git_command(["checkout", "master"], git_repo.local_repo_path)
        if "Branch master set up to track remote branch master from origin." not in output:
            raise GitRepositorySynchronizationException("Error in checking out master branch %s for local repository %s"
                                                        % (git_repo.repo_url, git_repo.local_repo_path))

        return True

    @staticmethod
    def init(path):
        output, errors = AgentGitHandler.execute_git_command(["init"], path)
        if "Initialized empty Git repository in" not in output:
            AgentGitHandler.log.exception("Initializing local repo at %s failed: %s" % (path, output))
            raise Exception("Initializing local repo at %s failed" % path)

    @staticmethod
    def is_valid_git_repository(git_repo):
        output, errors = AgentGitHandler.execute_git_command(["show-ref"], git_repo.local_repo_path)
        if len(output) > 0:
            refs = output.split("\n")
            for ref in refs:
                ref = ref.strip()
                if len(ref) > 0:
                    ref = ref.split(" ")
                    try:
                        AgentGitHandler.execute_git_command(["show", ref[0].strip()], git_repo.local_repo_path)
                    except RuntimeError:
                        return False
            return True
        else:
            return False

    @staticmethod
    def pull(git_repo):
        # check if modified files are present
        modified = AgentGitHandler.has_modified_files(git_repo.local_repo_path)
        if modified:
            if Config.is_commits_enabled:
                AgentGitHandler.log.debug(
                    "Un-staged files exist in working directory. Aborting git pull for this iteration...")
                return
            else:
                AgentGitHandler.log.warn("Changes detected in working directory but COMMIT_ENABLED is set to false!")
                AgentGitHandler.log.warn("Attempting to reset the working directory")
                AgentGitHandler.execute_git_command(["reset"], repo_path=git_repo.local_repo_path)

        # HEAD before pull
        (init_head, init_errors) = AgentGitHandler.execute_git_command(["rev-parse", "HEAD"], git_repo.local_repo_path)

        repo = Repo(git_repo.local_repo_path)
        AgentGitHandler.execute_git_command(["pull", "--rebase", "origin", git_repo.branch], git_repo.local_repo_path)
        AgentGitHandler.log.debug("Git pull rebase executed in checkout job")
        if repo.is_dirty():
            AgentGitHandler.log.error("Git pull operation in checkout job left the repository in dirty state")
            AgentGitHandler.log.error(
                "Git pull operation on remote %s for tenant %s failed" % (git_repo.repo_url, git_repo.tenant_id))

        # HEAD after pull
        (end_head, end_errors) = AgentGitHandler.execute_git_command(["rev-parse", "HEAD"], git_repo.local_repo_path)

        # check if HEAD was updated
        if init_head != end_head:
            AgentGitHandler.log.debug("Artifacts were updated as a result of the pull operation, thread: %s - %s" %
                                      (current_thread().getName(), current_thread().ident))

            return True
        else:
            return False

    @staticmethod
    def clone(git_repo):
        try:
            # create a temporary location to clone
            temp_repo_path = os.path.join(tempfile.gettempdir(), "pca_temp_" + git_repo.tenant_id)
            if os.path.isdir(temp_repo_path) and os.listdir(temp_repo_path) != []:
                GitUtils.delete_folder_tree(temp_repo_path)
                GitUtils.create_dir(temp_repo_path)

            # clone the repo to a temporary location first to avoid conflicts
            AgentGitHandler.log.debug(
                "Cloning artifacts from URL: %s to temp location: %s" % (git_repo.repo_url, temp_repo_path))
            Repo.clone_from(git_repo.auth_url, temp_repo_path, branch=git_repo.branch)

            # move the cloned dir to application path
            copy_tree(temp_repo_path, git_repo.local_repo_path)
            AgentGitHandler.log.info("Git clone operation for tenant %s successful" % git_repo.tenant_id)
            return git_repo
        except GitCommandError as e:
            raise GitRepositorySynchronizationException("Error while cloning repository for tenant %s: %s" % (
                git_repo.tenant_id, e))

    # @staticmethod
    # def checkout_remote_branch(git_repo):
    #     try:
    #         # create a temporary location to clone
    #         temp_repo_path = os.path.join(tempfile.gettempdir(), "pca_temp_" + git_repo.tenant_id)
    #         if os.path.isdir(temp_repo_path) and os.listdir(temp_repo_path) != []:
    #             GitUtils.delete_folder_tree(temp_repo_path)
    #             GitUtils.create_dir(temp_repo_path)
    #
    #         # clone the repo to a temporary location first to avoid conflicts
    #         AgentGitHandler.log.debug(
    #             "Cloning artifacts from URL: %s to temp location: %s" % (git_repo.repo_url, temp_repo_path))
    #         Repo.clone_from(git_repo.auth_url, temp_repo_path)
    #
    #         local_repo = Repo.init(temp_repo_path)
    #         origin = local_repo.create_remote('origin', local_repo.auth_url)
    #         assert origin.exists()
    #         # branch name = "tenant_<tenant_id>"
    #         origin.fetch("+refs/heads/" + git_repo.branch + ":refs/remotes/origin/" + git_repo.branch)
    #         AgentGitHandler.execute_git_command(["checkout", git_repo.branch], temp_repo_path)
    #
    #         # move the cloned dir to application path
    #         copy_tree(temp_repo_path, git_repo.local_repo_path)
    #         AgentGitHandler.log.info("Git clone operation for tenant %s successful" % git_repo.tenant_id)
    #         return git_repo
    #     except GitCommandError as e:
    #         raise GitRepositorySynchronizationException("Error while cloning repository for tenant %s: %s" % (
    #             git_repo.tenant_id, e))

    @staticmethod
    def retry_clone(git_repo):
        """Retry 'git clone' operation for defined number of attempts with defined intervals
        """
        git_clone_successful = False
        # Read properties from agent.conf
        max_retry_attempts = int(Config.artifact_clone_retry_count)
        retry_interval = int(Config.artifact_clone_retry_interval)
        retry_attempts = 0

        # Iterate until git clone is successful or reaches max retry attempts
        while git_clone_successful is False and retry_attempts < max_retry_attempts:
            try:
                retry_attempts += 1
                AgentGitHandler.clone(git_repo)
                AgentGitHandler.log.info(
                    "Retrying attempt to git clone operation for tenant %s successful" % git_repo.tenant_id)
                git_clone_successful = True
            except GitRepositorySynchronizationException as e:
                AgentGitHandler.log.exception("Retrying git clone attempt %s failed: %s" % (retry_attempts, e))
                if retry_attempts < max_retry_attempts:
                    time.sleep(retry_interval)
                else:
                    raise GitRepositorySynchronizationException("All attempts failed while retrying git clone: %s"
                                                                % e)

    @staticmethod
    def add_repo(git_repo):
        AgentGitHandler.__git_repositories[git_repo.tenant_id] = git_repo

    @staticmethod
    def get_repo(tenant_id):
        """

        :param int tenant_id:
        :return: GitRepository object
        :rtype: GitRepository
        """
        tenant_id = str(tenant_id)
        if tenant_id in AgentGitHandler.__git_repositories:
            return AgentGitHandler.__git_repositories[tenant_id]

        return None

    @staticmethod
    def clear_repo(tenant_id):
        if tenant_id in AgentGitHandler.__git_repositories:
            del AgentGitHandler.__git_repositories[tenant_id]

    @staticmethod
    def create_git_repo(repo_info, branch=None):
        git_repo = GitRepository()
        git_repo.tenant_id = repo_info.tenant_id
        git_repo.local_repo_path = repo_info.repo_path
        git_repo.repo_url = repo_info.repo_url
        git_repo.auth_url = AgentGitHandler.create_auth_url(repo_info)
        git_repo.repo_username = repo_info.repo_username
        git_repo.repo_password = repo_info.repo_password
        git_repo.commit_enabled = repo_info.commit_enabled
        if branch:
            git_repo.branch = branch
        else:
            git_repo.branch = 'master'


        git_repo.cloned = False

        return git_repo

    @staticmethod
    def create_auth_url(repo_info):
        # Accepted repo url formats
        # "https://host.com/path/to/repo.git"
        # "https://username@host.org/path/to/repo.git"
        # "https://username:password@host.org/path/to/repo.git" NOT RECOMMENDED
        # IMPORTANT: if the credentials are provided in the repo url, they must be url encoded
        if repo_info.repo_username is not None or repo_info.repo_password is not None:
            # credentials provided, have to modify url
            repo_url = repo_info.repo_url
            url_split = repo_url.split("://", 1)

            # urlencode repo username and password
            urlencoded_username = urllib.quote(repo_info.repo_username.strip(), safe='')
            urlencoded_password = urllib.quote(repo_info.repo_password.strip(), safe='')
            if "@" in url_split[1]:
                # credentials seem to be in the url, check
                at_split = url_split[1].split("@", 1)
                if ":" in at_split[0]:
                    # both username and password are in the url, return as is
                    return repo_info.repo_url
                else:
                    # only username is provided, need to include password
                    username_in_url = at_split[0].split(":", 1)[0]
                    return str(url_split[0] + "://" + username_in_url + ":" + urlencoded_password
                               + "@" + at_split[1])
            else:
                # no credentials in the url, need to include username and password
                return str(url_split[0] + "://" + urlencoded_username + ":" + urlencoded_password + "@" + url_split[1])
        # no credentials specified, return as is
        return repo_info.repo_url