import logging

from rbtools.api.errors import APIError
from rbtools.clients.errors import InvalidRevisionSpecError
from rbtools.commands import CommandError
from rbtools.utils.match_score import Score
from rbtools.utils.repository import get_repository_id
from rbtools.utils.users import get_user


def get_draft_or_current_value(field_name, review_request):
    """Returns the draft or current field value from a review request.

    If a draft exists for the supplied review request, return the draft's
    field value for the supplied field name, otherwise return the review
    request's field value for the supplied field name.
    """
    if review_request.draft:
        fields = review_request.draft[0]
    else:
        fields = review_request

    return fields[field_name]


def get_possible_matches(review_requests, summary, description, limit=5):
    """Returns a sorted list of tuples of score and review request.

    Each review request is given a score based on the summary and
    description provided. The result is a sorted list of tuples containing
    the score and the corresponding review request, sorted by the highest
    scoring review request first.
    """
    candidates = []

    # Get all potential matches.
    try:
        while True:
            for review_request in review_requests:
                summary_pair = (
                    get_draft_or_current_value(
                        'summary', review_request),
                    summary)
                description_pair = (
                    get_draft_or_current_value(
                        'description', review_request),
                    description)
                score = Score.get_match(summary_pair, description_pair)
                candidates.append((score, review_request))

            review_requests = review_requests.get_next()
    except StopIteration:
        pass

    # Sort by summary and description on descending rank.
    sorted_candidates = sorted(
        candidates,
        key=lambda m: (m[0].summary_score, m[0].description_score),
        reverse=True
    )

    return sorted_candidates[:limit]


def get_revisions(tool, cmd_args):
    """Returns the parsed revisions from the command line arguments.

    These revisions are used for diff generation and commit message
    extraction. They will be cached for future calls.
    """
    # Parse the provided revisions from the command line and generate
    # a spec or set of specialized extra arguments that the SCMClient
    # can use for diffing and commit lookups.
    try:
        revisions = tool.parse_revision_spec(cmd_args)
    except InvalidRevisionSpecError:
        if not tool.supports_diff_extra_args:
            raise

        revisions = None

    return revisions


def find_review_request_by_change_id(api_client, api_root, repository_info,
                                     repository_name, revisions):
    """Ask ReviewBoard for the review request ID for the tip revision.

    Note that this function calls the ReviewBoard API with the only_fields
    paramater, thus the returned review request will contain only the fields
    specified by the only_fields variable.

    If no review request is found, None will be returned instead.
    """
    only_fields = 'id,commit_id,changenum,status,url,absolute_url'
    change_id = revisions['tip']
    logging.debug('Attempting to find review request from tip revision ID: %s'
                  % change_id)
    # Strip off any prefix that might have been added by the SCM.
    change_id = change_id.split(':', 1)[1]

    optional_args = {}

    if change_id.isdigit():
        # Populate integer-only changenum field also for compatibility
        # with older API versions
        optional_args['changenum'] = int(change_id)

    user = get_user(api_client, api_root, auth_required=True)
    repository_id = get_repository_id(
        repository_info, api_root, repository_name)

    # Don't limit query to only pending requests because it's okay to stamp a
    # submitted review.
    review_requests = api_root.get_review_requests(repository=repository_id,
                                                   from_user=user.username,
                                                   commit_id=change_id,
                                                   only_links='self',
                                                   only_fields=only_fields,
                                                   **optional_args)

    if review_requests:
        count = review_requests.total_results

        # Only one review can be associated with a specific commit ID.
        if count > 0:
            assert count == 1, '%d review requests were returned' % count
            review_request = review_requests[0]
            logging.debug('Found review request %s with status %s'
                          % (review_request.id, review_request.status))

            if review_request.status != 'discarded':
                return review_request

    return None


def guess_existing_review_request(repository_info, repository_name,
                                  api_root, api_client, tool, revisions,
                                  guess_summary, guess_description,
                                  is_fuzzy_match_func=None,
                                  no_commit_error=None,
                                  submit_as=None):
    """Try to guess the existing review request ID if it is available.

    The existing review request is guessed by comparing the existing
    summary and description to the current post's summary and description,
    respectively. The current post's summary and description are guessed if
    they are not provided.

    If the summary and description exactly match those of an existing
    review request, that request is immediately returned. Otherwise,
    the user is prompted to select from a list of potential matches,
    sorted by the highest ranked match first.

    Note that this function calls the ReviewBoard API with the only_fields
    paramater, thus the returned review request will contain only the fields
    specified by the only_fields variable.
    """
    only_fields = 'id,summary,description,draft,url,absolute_url'

    if submit_as:
        username = submit_as
    else:
        user = get_user(api_client, api_root, auth_required=True)
        username = user.username

    repository_id = get_repository_id(
        repository_info, api_root, repository_name)

    try:
        # Get only pending requests by the current user for this
        # repository.
        review_requests = api_root.get_review_requests(
            repository=repository_id,
            from_user=username,
            status='pending',
            expand='draft',
            only_fields=only_fields,
            only_links='draft',
            show_all_unpublished=True)

        if not review_requests:
            raise CommandError('No existing review requests to update for '
                               'user %s.'
                               % user.username)
    except APIError as e:
        raise CommandError('Error getting review requests for user '
                           '%s: %s' % (user.username, e))

    summary = None
    description = None

    if not guess_summary or not guess_description:
        try:
            commit_message = tool.get_commit_message(revisions)

            if commit_message:
                if not guess_summary:
                    summary = commit_message['summary']

                if not guess_description:
                    description = commit_message['description']
            elif callable(no_commit_error):
                no_commit_error()
        except NotImplementedError:
            raise CommandError('--summary and --description are required.')

    if not summary and not description:
        return None

    possible_matches = get_possible_matches(review_requests, summary,
                                            description)
    exact_match_count = num_exact_matches(possible_matches)

    for score, review_request in possible_matches:
        # If the score is the only exact match, return the review request
        # ID without confirmation, otherwise prompt.
        if ((score.is_exact_match() and exact_match_count == 1) or
            (callable(is_fuzzy_match_func) and
             is_fuzzy_match_func(review_request))):
            return review_request

    return None


def num_exact_matches(possible_matches):
    """Returns the number of exact matches in the possible match list."""
    count = 0

    for score, request in possible_matches:
        if score.is_exact_match():
            count += 1

    return count
