#!/usr/bin/env python3
"""
Automatically tags branches base on commit message
"""
import logging

import semantic_version
import git

from auto_tag import constants
from auto_tag import git_custom_env


class AutoTag():
    """Class  wrapper for auto-tag functionality."""

    def __init__(self, repo, branch, upstream_remotes, detectors,
                 git_name=None, git_email=None, logger=None):
        """Initializa the AutoTag class.

        :param logger: If an existing logger is to be used
        :param args: CLI arguments
        """
        self._logger = logger or logging.getLogger(__name__)
        self._repo = repo
        self._detectors = detectors
        self._branch = branch
        self._upstream_remotes = upstream_remotes or []
        self._git_name = git_name
        self._git_email = git_email

    def _clean_tag_name(self, tag_name):
        """Remove common mistakes when using semantic versioning."""
        for prefix in constants.PREFIX_TO_ELIMINATE:
            if tag_name.startswith(prefix):
                clean_tag = tag_name[len(prefix):]
                self._logger.debug(
                    'Cleaning tag %s -> %s', tag_name, clean_tag)
                return clean_tag
        return tag_name

    def get_latest_tag(self, repo):
        """Return the last tag for the given repo in a Version class.
        :param repo: Repository to query for tags
        :type repo: git.Repo

        :returns: The latest tag from the repository
        :rtype: semantic_version.Version
        """
        latest_tag = None
        tags_name = [self._clean_tag_name(tag.name) for tag in repo.tags]
        sem_versions = [
            semantic_version.Version(tag_name) for tag_name in tags_name
        ]
        if sem_versions:
            latest_tag = max(sem_versions)
        self._logger.debug('Found latest tag %s', latest_tag)
        return latest_tag

    @staticmethod
    def bump_tag(tag, change_type):
        """Return the next tag version based on the change type."""
        if tag is None:
            return semantic_version.Version('0.0.1')

        if change_type == constants.MAJOR:
            return tag.next_major()

        if change_type == constants.MINOR:
            return tag.next_minor()

        return tag.next_patch()

    def get_all_commits_from_a_tag(self, repo, branch, tag):
        """
        Return all commits from the branch that
        happened father the specified tag.

        :param repo: Repository to query for commits
        :type repo: git.Repo

        :param branch: Branch to work on
        :type branch: str

        :param repo: Tag to stop the query at.
        :type repo: semantic_version.Version

        :returns: List of commits.
        :rtype: list of git.Commit
        """
        stop_commit = None
        if str(tag) in repo.tags:
            stop_commit = repo.tags[str(tag)].commit

        commits = []
        for commit in repo.iter_commits(rev=branch):
            if commit == stop_commit:
                break
            commits.append(commit)
        self._logger.debug(
            'Commits found from after tag %s: %s', tag, commits)
        return commits

    def get_change_type(self, commits):
        """Evaluate all detectors on a commit and decide on the change type."""
        change_type = constants.PATCH
        for commit in commits:
            for detector in self._detectors:
                if detector.evaluate(commit):
                    change_type = max(change_type, detector.change_type)

        return change_type

    @staticmethod
    def get_remote(repo, name):
        """Return the git.Remote object base on the name."""
        for remote in repo.remotes:
            if remote.name == name:
                return remote
        return None

    def push_to_remotes(self, repo, tag):
        """Push a tag to the specified remotes."""
        if self._upstream_remotes:
            self._logger.info('Start pushing to remotes: %s.',
                              self._upstream_remotes)
        else:
            self._logger.info('No push remote was specified')
            return
        for remote_name in self._upstream_remotes:
            remote = self.get_remote(repo, remote_name)
            if remote:
                self._logger.info('Push %s to %s', tag, remote)
                remote.push(str(tag))
            else:
                self._logger.error(
                    'Can\'t find remote with name `%s`', remote_name)

    @staticmethod
    def _create_tag_message(commits, tag):
        """Create a tag message that contains informations
        from the commits. """

        tag_message = 'Release {} \n\n'.format(str(tag))

        for message in [c.message for c in commits]:
            tag_message += '    * {}\n'.format(message.split('\n')[0].strip())
        return tag_message

    def _preapre_custom_environment(self):
        env_vars = {}
        if self._git_name is not None:
            env_vars['GIT_AUTHOR_NAME'] = self._git_name

        if self._git_email is not None:
            env_vars['GIT_AUTHOR_EMAIL'] = self._git_email
        return env_vars

    def work(self):
        """Main entry point.

        :param args: Argument to work on
        """
        repo = git.Repo(self._repo, odbt=git.GitDB)
        self._logger.info('Start tagging %s', repo)
        last_tag = self.get_latest_tag(repo)

        self._logger.info('Found tag %s', last_tag)
        commits = self.get_all_commits_from_a_tag(
            repo, self._branch, last_tag)
        type_of_change = self.get_change_type(commits)
        next_tag = self.bump_tag(last_tag, type_of_change)

        self._logger.info('Bumping tag %s -> %s', last_tag, next_tag)

        with git_custom_env.GitCustomeEnvironment(
                repo.working_dir, self._git_name, self._git_email):
            repo.create_tag(
                str(next_tag),
                message=self._create_tag_message(commits, next_tag))

        self.push_to_remotes(repo, next_tag)
