# Copyright (c) 2020 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import os
import random

import gerrit_util
from gerrit_cache import GerritCache
import git_common


class OwnersClient(object):
    """Interact with OWNERS files in a repository.

    This class allows you to interact with OWNERS files in a repository both the
    Gerrit Code-Owners plugin REST API, and the owners database implemented by
    Depot Tools in owners.py:

        - List all the owners for a group of files.
        - Check if files have been approved.
        - Suggest owners for a group of files.

    All code should use this class to interact with OWNERS files instead of the
    owners database in owners.py
    """
    # '*' means that everyone can approve.
    EVERYONE = '*'

    # Possible status of a file.
    # - INSUFFICIENT_REVIEWERS: The path needs owners approval, but none of its
    #   owners is currently a reviewer of the change.
    # - PENDING: An owner of this path has been added as reviewer, but approval
    #   has not been given yet.
    # - APPROVED: The path has been approved by an owner.
    APPROVED = 'APPROVED'
    PENDING = 'PENDING'
    INSUFFICIENT_REVIEWERS = 'INSUFFICIENT_REVIEWERS'

    def ListOwners(self, path):
        """List all owners for a file.

        The returned list is sorted so that better owners appear first.
        """
        raise Exception('Not implemented')

    def BatchListOwners(self, paths):
        """List all owners for a group of files.

        Returns a dictionary {path: [owners]}.
        """
        if not paths:
            return dict()
        nproc = min(gerrit_util.MAX_CONCURRENT_CONNECTION, len(paths))
        with git_common.ScopedPool(nproc, kind='threads') as pool:
            return dict(
                pool.imap_unordered(lambda p: (p, self.ListOwners(p)), paths))

    def GetFilesApprovalStatus(self, paths, approvers, reviewers):
        """Check the approval status for the given paths.

        Utility method to check for approval status when a change has not yet
        been created, given reviewers and approvers.

        See GetChangeApprovalStatus for description of the returned value.
        """
        approvers = set(approvers)
        if approvers:
            approvers.add(self.EVERYONE)
        reviewers = set(reviewers)
        if reviewers:
            reviewers.add(self.EVERYONE)
        status = {}
        owners_by_path = self.BatchListOwners(paths)
        for path, owners in owners_by_path.items():
            owners = set(owners)
            if owners.intersection(approvers):
                status[path] = self.APPROVED
            elif owners.intersection(reviewers):
                status[path] = self.PENDING
            else:
                status[path] = self.INSUFFICIENT_REVIEWERS
        return status

    def ScoreOwners(self, paths, exclude=None):
        """Get sorted list of owners for the given paths."""
        if not paths:
            return []
        exclude = exclude or []
        owners = []
        queues = self.BatchListOwners(paths).values()
        for i in range(max(len(q) for q in queues)):
            for q in queues:
                if i < len(q) and q[i] not in owners and q[i] not in exclude:
                    owners.append(q[i])
        return owners

    def SuggestOwners(self, paths, exclude=None):
        """Suggest a set of owners for the given paths."""
        exclude = exclude or []

        paths_by_owner = {}
        owners_by_path = self.BatchListOwners(paths)
        for path, owners in owners_by_path.items():
            for owner in owners:
                paths_by_owner.setdefault(owner, set()).add(path)

        selected = []
        missing = set(paths)
        for owner in self.ScoreOwners(paths, exclude=exclude):
            missing_len = len(missing)
            missing.difference_update(paths_by_owner[owner])
            if missing_len > len(missing):
                selected.append(owner)
            if not missing:
                break

        return selected

    def SuggestMinimalOwners(self,
                             paths: list[str],
                             exclude: list[str] = None) -> list[str]:
        """
        Suggest a set of owners for the given paths. Never return an owner in
        the |exclude| list.

        Aims to provide only one, but will provide more if it's unable to
        find a common owner.
        """
        exclude = exclude or []

        owners_by_path = self.BatchListOwners(paths)
        if not owners_by_path:
            return []

        common_owners = set(owners_by_path.popitem()[1]) - set(exclude)
        for _, owners in owners_by_path.items():
            common_owners = common_owners.intersection(set(owners))

        if not common_owners:
            # This likely means some of the files had `noparent` set.
            # Fall back to the default suggestion algorithm, which accounts
            # for noparent but is liable to return many different owners
            return self.SuggestOwners(paths, exclude)

        # Return an arbitrary common owner, preferring those with a good score
        sorted_common_owners = [
            owner for owner in self.ScoreOwners(paths, exclude=exclude)
            if owner in common_owners
        ]

        # Return a singleton list so this function has a consistent return type
        return sorted_common_owners[:1]


class GerritClient(OwnersClient):
    """Implement OwnersClient using OWNERS REST API."""
    def __init__(self, host, project, branch):
        super(GerritClient, self).__init__()

        self._host = host
        self._project = project
        self._branch = branch
        self._owners_cache = {}
        self._best_owners_cache = {}

        # Seed used by Gerrit to shuffle code owners that have the same score.
        # Can be used to make the sort order stable across several requests,
        # e.g. to get the same set of random code owners for different file
        # paths that have the same code owners.
        self._seed = random.getrandbits(30)

    def _FetchOwners(self, path, cache, highest_score_only=False):
        # Always use slashes as separators.
        path = path.replace(os.sep, '/')
        if path not in cache:
            # GetOwnersForFile returns a list of account details sorted by order
            # of best reviewer for path. If owners have the same score, the
            # order is random, seeded by `self._seed`.
            data = gerrit_util.GetOwnersForFile(
                self._host,
                self._project,
                self._branch,
                path,
                resolve_all_users=False,
                highest_score_only=highest_score_only,
                seed=self._seed)
            cache[path] = [
                d['account']['email'] for d in data['code_owners']
                if 'account' in d and 'email' in d['account']
            ]
            # If owned_by_all_users is true, add everyone as an owner at the end
            # of the owners list.
            if data.get('owned_by_all_users', False):
                cache[path].append(self.EVERYONE)
        return cache[path]

    def ListOwners(self, path):
        return self._FetchOwners(path, self._owners_cache)

    def ListBestOwners(self, path):
        return self._FetchOwners(path,
                                 self._best_owners_cache,
                                 highest_score_only=True)

    def BatchListBestOwners(self, paths):
        """List only the higest-scoring owners for a group of files.

        Returns a dictionary {path: [owners]}.
        """
        with git_common.ScopedPool(kind='threads') as pool:
            return dict(
                pool.imap_unordered(lambda p: (p, self.ListBestOwners(p)),
                                    paths))


def GetCodeOwnersClient(host, project, branch, cache=None):
    """Get a new OwnersClient.

    Uses GerritClient and raises an exception if code-owners plugin is not
    available."""
    key = GerritCache.get_host_code_owners_enabled_key(host)
    is_enabled = None
    if cache:
        is_enabled = cache.getBoolean(key)

    if is_enabled is None:
        is_enabled = gerrit_util.IsCodeOwnersEnabledOnHost(host)
        if cache:
            cache.setBoolean(key, is_enabled)

    if is_enabled:
        return GerritClient(host, project, branch)
    raise Exception(
        'code-owners plugin is not enabled. Ask your host admin to enable it '
        'on %s. Read more about code-owners at '
        'https://chromium-review.googlesource.com/'
        'plugins/code-owners/Documentation/index.html.' % host)
