# -*- coding: utf-8 -*-

# Copyright (C) 2013-2018 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/


"""
Set of hooks run by RhodeCode Enterprise
"""

import os
import collections
import logging

import rhodecode
from rhodecode import events
from rhodecode.lib import helpers as h
from rhodecode.lib import audit_logger
from rhodecode.lib.utils2 import safe_str
from rhodecode.lib.exceptions import HTTPLockedRC, UserCreationError
from rhodecode.model.db import Repository, User

log = logging.getLogger(__name__)


HookResponse = collections.namedtuple('HookResponse', ('status', 'output'))


def is_shadow_repo(extras):
    """
    Returns ``True`` if this is an action executed against a shadow repository.
    """
    return extras['is_shadow_repo']


def _get_scm_size(alias, root_path):

    if not alias.startswith('.'):
        alias += '.'

    size_scm, size_root = 0, 0
    for path, unused_dirs, files in os.walk(safe_str(root_path)):
        if path.find(alias) != -1:
            for f in files:
                try:
                    size_scm += os.path.getsize(os.path.join(path, f))
                except OSError:
                    pass
        else:
            for f in files:
                try:
                    size_root += os.path.getsize(os.path.join(path, f))
                except OSError:
                    pass

    size_scm_f = h.format_byte_size_binary(size_scm)
    size_root_f = h.format_byte_size_binary(size_root)
    size_total_f = h.format_byte_size_binary(size_root + size_scm)

    return size_scm_f, size_root_f, size_total_f


# actual hooks called by Mercurial internally, and GIT by our Python Hooks
def repo_size(extras):
    """Present size of repository after push."""
    repo = Repository.get_by_repo_name(extras.repository)
    vcs_part = safe_str(u'.%s' % repo.repo_type)
    size_vcs, size_root, size_total = _get_scm_size(vcs_part,
                                                    repo.repo_full_path)
    msg = ('Repository `%s` size summary %s:%s repo:%s total:%s\n'
           % (repo.repo_name, vcs_part, size_vcs, size_root, size_total))
    return HookResponse(0, msg)


def pre_push(extras):
    """
    Hook executed before pushing code.

    It bans pushing when the repository is locked.
    """

    usr = User.get_by_username(extras.username)
    output = ''
    if extras.locked_by[0] and usr.user_id != int(extras.locked_by[0]):
        locked_by = User.get(extras.locked_by[0]).username
        reason = extras.locked_by[2]
        # this exception is interpreted in git/hg middlewares and based
        # on that proper return code is server to client
        _http_ret = HTTPLockedRC(
            _locked_by_explanation(extras.repository, locked_by, reason))
        if str(_http_ret.code).startswith('2'):
            # 2xx Codes don't raise exceptions
            output = _http_ret.title
        else:
            raise _http_ret

    # Propagate to external components. This is done after checking the
    # lock, for consistent behavior.
    if not is_shadow_repo(extras):
        pre_push_extension(repo_store_path=Repository.base_path(), **extras)
        events.trigger(events.RepoPrePushEvent(
            repo_name=extras.repository, extras=extras))

    return HookResponse(0, output)


def pre_pull(extras):
    """
    Hook executed before pulling the code.

    It bans pulling when the repository is locked.
    """

    output = ''
    if extras.locked_by[0]:
        locked_by = User.get(extras.locked_by[0]).username
        reason = extras.locked_by[2]
        # this exception is interpreted in git/hg middlewares and based
        # on that proper return code is server to client
        _http_ret = HTTPLockedRC(
            _locked_by_explanation(extras.repository, locked_by, reason))
        if str(_http_ret.code).startswith('2'):
            # 2xx Codes don't raise exceptions
            output = _http_ret.title
        else:
            raise _http_ret

    # Propagate to external components. This is done after checking the
    # lock, for consistent behavior.
    if not is_shadow_repo(extras):
        pre_pull_extension(**extras)
        events.trigger(events.RepoPrePullEvent(
            repo_name=extras.repository, extras=extras))

    return HookResponse(0, output)


def post_pull(extras):
    """Hook executed after client pulls the code."""

    audit_user = audit_logger.UserWrap(
        username=extras.username,
        ip_addr=extras.ip)
    repo = audit_logger.RepoWrap(repo_name=extras.repository)
    audit_logger.store(
        'user.pull', action_data={
            'user_agent': extras.user_agent},
        user=audit_user, repo=repo, commit=True)

    # Propagate to external components.
    if not is_shadow_repo(extras):
        post_pull_extension(**extras)
        events.trigger(events.RepoPullEvent(
            repo_name=extras.repository, extras=extras))

    output = ''
    # make lock is a tri state False, True, None. We only make lock on True
    if extras.make_lock is True and not is_shadow_repo(extras):
        user = User.get_by_username(extras.username)
        Repository.lock(Repository.get_by_repo_name(extras.repository),
                        user.user_id,
                        lock_reason=Repository.LOCK_PULL)
        msg = 'Made lock on repo `%s`' % (extras.repository,)
        output += msg

    if extras.locked_by[0]:
        locked_by = User.get(extras.locked_by[0]).username
        reason = extras.locked_by[2]
        _http_ret = HTTPLockedRC(
            _locked_by_explanation(extras.repository, locked_by, reason))
        if str(_http_ret.code).startswith('2'):
            # 2xx Codes don't raise exceptions
            output += _http_ret.title

    return HookResponse(0, output)


def post_push(extras):
    """Hook executed after user pushes to the repository."""
    commit_ids = extras.commit_ids

    # log the push call
    audit_user = audit_logger.UserWrap(
        username=extras.username, ip_addr=extras.ip)
    repo = audit_logger.RepoWrap(repo_name=extras.repository)
    audit_logger.store(
        'user.push', action_data={
            'user_agent': extras.user_agent,
            'commit_ids': commit_ids[:400]},
        user=audit_user, repo=repo, commit=True)

    # Propagate to external components.
    if not is_shadow_repo(extras):
        post_push_extension(
            repo_store_path=Repository.base_path(),
            pushed_revs=commit_ids,
            **extras)
        events.trigger(events.RepoPushEvent(
            repo_name=extras.repository,
            pushed_commit_ids=commit_ids,
            extras=extras))

    output = ''
    # make lock is a tri state False, True, None. We only release lock on False
    if extras.make_lock is False and not is_shadow_repo(extras):
        Repository.unlock(Repository.get_by_repo_name(extras.repository))
        msg = 'Released lock on repo `%s`\n' % extras.repository
        output += msg

    if extras.locked_by[0]:
        locked_by = User.get(extras.locked_by[0]).username
        reason = extras.locked_by[2]
        _http_ret = HTTPLockedRC(
            _locked_by_explanation(extras.repository, locked_by, reason))
        # TODO: johbo: if not?
        if str(_http_ret.code).startswith('2'):
            # 2xx Codes don't raise exceptions
            output += _http_ret.title

    if extras.new_refs:
        tmpl = \
            extras.server_url + '/' + \
            extras.repository + \
            "/pull-request/new?{ref_type}={ref_name}"
        for branch_name in extras.new_refs['branches']:
            output += 'RhodeCode: open pull request link: {}\n'.format(
                tmpl.format(ref_type='branch', ref_name=branch_name))

        for book_name in extras.new_refs['bookmarks']:
            output += 'RhodeCode: open pull request link: {}\n'.format(
                tmpl.format(ref_type='bookmark', ref_name=book_name))

    output += 'RhodeCode: push completed\n'
    return HookResponse(0, output)


def _locked_by_explanation(repo_name, user_name, reason):
    message = (
        'Repository `%s` locked by user `%s`. Reason:`%s`'
        % (repo_name, user_name, reason))
    return message


def check_allowed_create_user(user_dict, created_by, **kwargs):
    # pre create hooks
    if pre_create_user.is_active():
        allowed, reason = pre_create_user(created_by=created_by, **user_dict)
        if not allowed:
            raise UserCreationError(reason)


class ExtensionCallback(object):
    """
    Forwards a given call to rcextensions, sanitizes keyword arguments.

    Does check if there is an extension active for that hook. If it is
    there, it will forward all `kwargs_keys` keyword arguments to the
    extension callback.
    """

    def __init__(self, hook_name, kwargs_keys):
        self._hook_name = hook_name
        self._kwargs_keys = set(kwargs_keys)

    def __call__(self, *args, **kwargs):
        log.debug('Calling extension callback for %s', self._hook_name)

        kwargs_to_pass = dict((key, kwargs[key]) for key in self._kwargs_keys)
        # backward compat for removed api_key for old hooks. THis was it works
        # with older rcextensions that require api_key present
        if self._hook_name in ['CREATE_USER_HOOK', 'DELETE_USER_HOOK']:
            kwargs_to_pass['api_key'] = '_DEPRECATED_'

        callback = self._get_callback()
        if callback:
            return callback(**kwargs_to_pass)
        else:
            log.debug('extensions callback not found skipping...')

    def is_active(self):
        return hasattr(rhodecode.EXTENSIONS, self._hook_name)

    def _get_callback(self):
        return getattr(rhodecode.EXTENSIONS, self._hook_name, None)


pre_pull_extension = ExtensionCallback(
    hook_name='PRE_PULL_HOOK',
    kwargs_keys=(
        'server_url', 'config', 'scm', 'username', 'ip', 'action',
        'repository'))


post_pull_extension = ExtensionCallback(
    hook_name='PULL_HOOK',
    kwargs_keys=(
        'server_url', 'config', 'scm', 'username', 'ip', 'action',
        'repository'))


pre_push_extension = ExtensionCallback(
    hook_name='PRE_PUSH_HOOK',
    kwargs_keys=(
        'server_url', 'config', 'scm', 'username', 'ip', 'action',
        'repository', 'repo_store_path', 'commit_ids'))


post_push_extension = ExtensionCallback(
    hook_name='PUSH_HOOK',
    kwargs_keys=(
        'server_url', 'config', 'scm', 'username', 'ip', 'action',
        'repository', 'repo_store_path', 'pushed_revs'))


pre_create_user = ExtensionCallback(
    hook_name='PRE_CREATE_USER_HOOK',
    kwargs_keys=(
        'username', 'password', 'email', 'firstname', 'lastname', 'active',
        'admin', 'created_by'))


log_create_pull_request = ExtensionCallback(
    hook_name='CREATE_PULL_REQUEST',
    kwargs_keys=(
        'server_url', 'config', 'scm', 'username', 'ip', 'action',
        'repository', 'pull_request_id', 'url', 'title', 'description',
        'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
        'mergeable', 'source', 'target', 'author', 'reviewers'))


log_merge_pull_request = ExtensionCallback(
    hook_name='MERGE_PULL_REQUEST',
    kwargs_keys=(
        'server_url', 'config', 'scm', 'username', 'ip', 'action',
        'repository', 'pull_request_id', 'url', 'title', 'description',
        'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
        'mergeable', 'source', 'target', 'author', 'reviewers'))


log_close_pull_request = ExtensionCallback(
    hook_name='CLOSE_PULL_REQUEST',
    kwargs_keys=(
        'server_url', 'config', 'scm', 'username', 'ip', 'action',
        'repository', 'pull_request_id', 'url', 'title', 'description',
        'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
        'mergeable', 'source', 'target', 'author', 'reviewers'))


log_review_pull_request = ExtensionCallback(
    hook_name='REVIEW_PULL_REQUEST',
    kwargs_keys=(
        'server_url', 'config', 'scm', 'username', 'ip', 'action',
        'repository', 'pull_request_id', 'url', 'title', 'description',
        'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
        'mergeable', 'source', 'target', 'author', 'reviewers'))


log_update_pull_request = ExtensionCallback(
    hook_name='UPDATE_PULL_REQUEST',
    kwargs_keys=(
        'server_url', 'config', 'scm', 'username', 'ip', 'action',
        'repository', 'pull_request_id', 'url', 'title', 'description',
        'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
        'mergeable', 'source', 'target', 'author', 'reviewers'))


log_create_user = ExtensionCallback(
    hook_name='CREATE_USER_HOOK',
    kwargs_keys=(
        'username', 'full_name_or_username', 'full_contact', 'user_id',
        'name', 'firstname', 'short_contact', 'admin', 'lastname',
        'ip_addresses', 'extern_type', 'extern_name',
        'email', 'api_keys', 'last_login',
        'full_name', 'active', 'password', 'emails',
        'inherit_default_permissions', 'created_by', 'created_on'))


log_delete_user = ExtensionCallback(
    hook_name='DELETE_USER_HOOK',
    kwargs_keys=(
        'username', 'full_name_or_username', 'full_contact', 'user_id',
        'name', 'firstname', 'short_contact', 'admin', 'lastname',
        'ip_addresses',
        'email', 'last_login',
        'full_name', 'active', 'password', 'emails',
        'inherit_default_permissions', 'deleted_by'))


log_create_repository = ExtensionCallback(
    hook_name='CREATE_REPO_HOOK',
    kwargs_keys=(
        'repo_name', 'repo_type', 'description', 'private', 'created_on',
        'enable_downloads', 'repo_id', 'user_id', 'enable_statistics',
        'clone_uri', 'fork_id', 'group_id', 'created_by'))


log_delete_repository = ExtensionCallback(
    hook_name='DELETE_REPO_HOOK',
    kwargs_keys=(
        'repo_name', 'repo_type', 'description', 'private', 'created_on',
        'enable_downloads', 'repo_id', 'user_id', 'enable_statistics',
        'clone_uri', 'fork_id', 'group_id', 'deleted_by', 'deleted_on'))


log_create_repository_group = ExtensionCallback(
    hook_name='CREATE_REPO_GROUP_HOOK',
    kwargs_keys=(
        'group_name', 'group_parent_id', 'group_description',
        'group_id', 'user_id', 'created_by', 'created_on',
        'enable_locking'))