# Copyright (C) 2014-2023 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/

"""
JSON RPC utils
"""

import collections
import logging

from rhodecode.api.exc import JSONRPCError
from rhodecode.lib.auth import (
    HasPermissionAnyApi, HasRepoPermissionAnyApi, HasRepoGroupPermissionAnyApi)
from rhodecode.lib.str_utils import safe_str
from rhodecode.lib.vcs.exceptions import RepositoryError
from rhodecode.lib.view_utils import get_commit_from_ref_name
from rhodecode.lib.utils2 import str2bool

log = logging.getLogger(__name__)


class OAttr(object):
    """
    Special Option that defines other attribute, and can default to them

    Example::

        def test(apiuser, userid=Optional(OAttr('apiuser')):
            user = Optional.extract(userid, evaluate_locals=local())
            #if we pass in userid, we get it, else it will default to apiuser
            #attribute
    """

    def __init__(self, attr_name):
        self.attr_name = attr_name

    def __repr__(self):
        return '<OptionalAttr:%s>' % self.attr_name

    def __call__(self):
        return self


class Optional(object):
    """
    Defines an optional parameter::

        param = param.getval() if isinstance(param, Optional) else param
        param = param() if isinstance(param, Optional) else param

    is equivalent of::

        param = Optional.extract(param)

    """

    def __init__(self, type_):
        self.type_ = type_

    def __repr__(self):
        return '<Optional:%s>' % self.type_.__repr__()

    def __call__(self):
        return self.getval()

    def getval(self, evaluate_locals=None):
        """
        returns value from this Optional instance
        """
        if isinstance(self.type_, OAttr):
            param_name = self.type_.attr_name
            if evaluate_locals:
                return evaluate_locals[param_name]
            # use params name
            return param_name
        return self.type_

    @classmethod
    def extract(cls, val, evaluate_locals=None, binary=None):
        """
        Extracts value from Optional() instance

        :param val:
        :return: original value if it's not Optional instance else
            value of instance
        """
        if isinstance(val, cls):
            val = val.getval(evaluate_locals)

        if binary:
            val = str2bool(val)

        return val


def parse_args(cli_args, key_prefix=''):
    from rhodecode.lib.utils2 import (escape_split)
    kwargs = collections.defaultdict(dict)
    for el in escape_split(cli_args, ','):
        kv = escape_split(el, '=', 1)
        if len(kv) == 2:
            k, v = kv
            kwargs[key_prefix + k] = v
    return kwargs


def get_origin(obj):
    """
    Get origin of permission from object.

    :param obj:
    """
    origin = 'permission'

    if getattr(obj, 'owner_row', '') and getattr(obj, 'admin_row', ''):
        # admin and owner case, maybe we should use dual string ?
        origin = 'owner'
    elif getattr(obj, 'owner_row', ''):
        origin = 'owner'
    elif getattr(obj, 'admin_row', ''):
        origin = 'super-admin'
    return origin


def store_update(updates, attr, name):
    """
    Stores param in updates dict if it's not instance of Optional
    allows easy updates of passed in params
    """
    if not isinstance(attr, Optional):
        updates[name] = attr


def has_superadmin_permission(apiuser):
    """
    Return True if apiuser is admin or return False

    :param apiuser:
    """
    if HasPermissionAnyApi('hg.admin')(user=apiuser):
        return True
    return False


def validate_repo_permissions(apiuser, repoid, repo, perms):
    """
    Raise JsonRPCError if apiuser is not authorized or return True

    :param apiuser:
    :param repoid:
    :param repo:
    :param perms:
    """
    if not HasRepoPermissionAnyApi(*perms)(
            user=apiuser, repo_name=repo.repo_name):
        raise JSONRPCError('repository `%s` does not exist' % repoid)

    return True


def validate_repo_group_permissions(apiuser, repogroupid, repo_group, perms):
    """
    Raise JsonRPCError if apiuser is not authorized or return True

    :param apiuser:
    :param repogroupid: just the id of repository group
    :param repo_group: instance of repo_group
    :param perms:
    """
    if not HasRepoGroupPermissionAnyApi(*perms)(
            user=apiuser, group_name=repo_group.group_name):
        raise JSONRPCError(
            'repository group `%s` does not exist' % repogroupid)

    return True


def validate_set_owner_permissions(apiuser, owner):
    if isinstance(owner, Optional):
        owner = get_user_or_error(apiuser.user_id)
    else:
        if has_superadmin_permission(apiuser):
            owner = get_user_or_error(owner)
        else:
            # forbid setting owner for non-admins
            raise JSONRPCError(
                'Only RhodeCode super-admin can specify `owner` param')
    return owner


def get_user_or_error(userid):
    """
    Get user by id or name or return JsonRPCError if not found

    :param userid:
    """
    from rhodecode.model.user import UserModel
    user_model = UserModel()

    if isinstance(userid, int):
        try:
            user = user_model.get_user(userid)
        except ValueError:
            user = None
    else:
        user = user_model.get_by_username(userid)

    if user is None:
        raise JSONRPCError(
            'user `{}` does not exist'.format(userid))
    return user


def get_repo_or_error(repoid):
    """
    Get repo by id or name or return JsonRPCError if not found

    :param repoid:
    """
    from rhodecode.model.repo import RepoModel
    repo_model = RepoModel()

    if isinstance(repoid, int):
        try:
            repo = repo_model.get_repo(repoid)
        except ValueError:
            repo = None
    else:
        repo = repo_model.get_by_repo_name(repoid)

    if repo is None:
        raise JSONRPCError(
            'repository `{}` does not exist'.format(repoid))
    return repo


def get_repo_group_or_error(repogroupid):
    """
    Get repo group by id or name or return JsonRPCError if not found

    :param repogroupid:
    """
    from rhodecode.model.repo_group import RepoGroupModel
    repo_group_model = RepoGroupModel()

    if isinstance(repogroupid, int):
        try:
            repo_group = repo_group_model._get_repo_group(repogroupid)
        except ValueError:
            repo_group = None
    else:
        repo_group = repo_group_model.get_by_group_name(repogroupid)

    if repo_group is None:
        raise JSONRPCError(
            'repository group `{}` does not exist'.format(repogroupid))
    return repo_group


def get_user_group_or_error(usergroupid):
    """
    Get user group by id or name or return JsonRPCError if not found

    :param usergroupid:
    """
    from rhodecode.model.user_group import UserGroupModel
    user_group_model = UserGroupModel()

    if isinstance(usergroupid, int):
        try:
            user_group = user_group_model.get_group(usergroupid)
        except ValueError:
            user_group = None
    else:
        user_group = user_group_model.get_by_name(usergroupid)

    if user_group is None:
        raise JSONRPCError(
            'user group `{}` does not exist'.format(usergroupid))
    return user_group


def get_perm_or_error(permid, prefix=None):
    """
    Get permission by id or name or return JsonRPCError if not found

    :param permid:
    """
    from rhodecode.model.permission import PermissionModel

    perm = PermissionModel.cls.get_by_key(permid)
    if perm is None:
        msg = f'permission `{permid}` does not exist.'
        if prefix:
            msg += f' Permission should start with prefix: `{prefix}`'
        raise JSONRPCError(msg)

    if prefix:
        if not perm.permission_name.startswith(prefix):
            raise JSONRPCError('permission `%s` is invalid, '
                               'should start with %s' % (permid, prefix))
    return perm


def get_gist_or_error(gistid):
    """
    Get gist by id or gist_access_id or return JsonRPCError if not found

    :param gistid:
    """
    from rhodecode.model.gist import GistModel

    gist = GistModel.cls.get_by_access_id(gistid)
    if gist is None:
        raise JSONRPCError('gist `{}` does not exist'.format(gistid))
    return gist


def get_pull_request_or_error(pullrequestid):
    """
    Get pull request by id or return JsonRPCError if not found

    :param pullrequestid:
    """
    from rhodecode.model.pull_request import PullRequestModel

    try:
        pull_request = PullRequestModel().get(int(pullrequestid))
    except ValueError:
        raise JSONRPCError('pullrequestid must be an integer')
    if not pull_request:
        raise JSONRPCError('pull request `{}` does not exist'.format(
            pullrequestid))
    return pull_request


def build_commit_data(rhodecode_vcs_repo, commit, detail_level):
    commit2 = commit
    commit1 = commit.first_parent

    parsed_diff = []
    if detail_level == 'extended':
        for f_path in commit.added_paths:
            parsed_diff.append(_get_commit_dict(filename=f_path, op='A'))
        for f_path in commit.changed_paths:
            parsed_diff.append(_get_commit_dict(filename=f_path, op='M'))
        for f_path in commit.removed_paths:
            parsed_diff.append(_get_commit_dict(filename=f_path, op='D'))

    elif detail_level == 'full':
        from rhodecode.lib import diffs

        _diff = rhodecode_vcs_repo.get_diff(commit1, commit2,)
        diff_processor = diffs.DiffProcessor(_diff, diff_format='newdiff', show_full_diff=True)

        for dp in diff_processor.prepare():
            del dp['stats']['ops']
            _stats = dp['stats']
            parsed_diff.append(_get_commit_dict(
                filename=dp['filename'], op=dp['operation'],
                new_revision=dp['new_revision'],
                old_revision=dp['old_revision'],
                raw_diff=dp['raw_diff'], stats=_stats))

    return parsed_diff


def get_commit_or_error(ref, repo):
    try:
        ref_type, _, ref_hash = ref.split(':')
    except ValueError:
        raise JSONRPCError(
            'Ref `{ref}` given in a wrong format. Please check the API'
            ' documentation for more details'.format(ref=ref))
    try:
        # TODO: dan: refactor this to use repo.scm_instance().get_commit()
        # once get_commit supports ref_types
        return get_commit_from_ref_name(repo, ref_hash)
    except RepositoryError:
        raise JSONRPCError(f'Ref `{ref}` does not exist')


def _get_ref_hash(repo, type_, name):
    vcs_repo = repo.scm_instance()
    if type_ in ['branch'] and vcs_repo.alias in ('hg', 'git'):
        return vcs_repo.branches[name]
    elif type_ in ['bookmark', 'book'] and vcs_repo.alias == 'hg':
        return vcs_repo.bookmarks[name]
    else:
        raise ValueError()


def resolve_ref_or_error(ref, repo, allowed_ref_types=None):
    allowed_ref_types = allowed_ref_types or ['bookmark', 'book', 'tag', 'branch']

    def _parse_ref(type_, name, hash_=None):
        return type_, name, hash_

    try:
        ref_type, ref_name, ref_hash = _parse_ref(*ref.split(':'))
    except TypeError:
        raise JSONRPCError(
            'Ref `{ref}` given in a wrong format. Please check the API'
            ' documentation for more details'.format(ref=ref))

    if ref_type not in allowed_ref_types:
        raise JSONRPCError(
            'Ref `{ref}` type is not allowed. '
            'Only:{allowed_refs} are possible.'.format(
                ref=ref, allowed_refs=allowed_ref_types))

    try:
        ref_hash = ref_hash or _get_ref_hash(repo, ref_type, ref_name)
    except (KeyError, ValueError):
        raise JSONRPCError(
            'The specified value:{type}:`{name}` does not exist, or is not allowed.'.format(
                type=ref_type, name=ref_name))

    return ':'.join([ref_type, ref_name, ref_hash])


def _get_commit_dict(
        filename, op, new_revision=None, old_revision=None,
        raw_diff=None, stats=None):
    if stats is None:
        stats = {
            "added": None,
            "binary": None,
            "deleted": None
        }
    return {
        "filename": safe_str(filename),
        "op": op,

        # extra details
        "new_revision": new_revision,
        "old_revision": old_revision,

        "raw_diff": raw_diff,
        "stats": stats
    }
