#!/usr/bin/env python3
#
# Trigger script for connecting Perforce servers to Review Board.
#
# This can check changes for approval in Review Board, auto-close review
# requests, and stamp changesets with the review request URL.
#
# Usage:
#
#     $ p4-trigger-script [options] %change%
#
#     Connection information and options must either be provided by editing
#     this script or specifying as command line options.
#
#
# To install, run:
#     $ p4 triggers
#     Triggers:
#         reviewboard change-submit //depot/... "/path/to/python /path/to/p4-trigger-script [options] %change%"
#
#     If you're using this with the RBTools for Windows installer, specify
#     the path to the bundled Python.exe:

#         reviewboard change-submit //depot/... "C:\Program Files\RBTools\Python\python.exe C:\Path/To/p4-trigger-script [options] %change%"
#
#
# Required Options:
#
#     --server or REVIEWBOARD_URL
#     --username or REVIEWBOARD_USERNAME
#     --api-token, --password, REVIEWBOARD_API_TOKEN, or REVIEWBOARD_PASSWORD
#     --p4-port or P4_PORT
#     --p4-user or P4_USER
#
#     Additional options may also be needed for your environment.
#
#
# Choosing the Perforce user:
#
#     This script may either need to operate as the submitting user, or as an
#     administrative user.
#
#     The correct option depends on your environment. If the Perforce client
#     spec specifies "Host:", the Perforce server may not be able to stamp
#     changes.
#
#     To stamp as the user submitting the change, specify %user%,
#     %client%, and %clienthost% as arguments:
#
#         p4-trigger-script --p4-user %user%
#                           --p4-client %client%
#                           --p4-host %clienthost%
#
#     You can also specify --p4-host to set an explicit $P4HOST value.
#     Perforce does not provide the user's Host as a value.
#
#     For variables available to trigger scripts, see:
#
#     https://www.perforce.com/manuals/p4sag/Content/P4SAG/scripting.triggers.variables.html


import argparse
import logging
import os
import sys

# Configure the Python Path, if needed:
#
# sys.path.insert(0, '...')

# Configure the executable search path, if necessary.
#
# os.environ['PATH'] = '...' + os.environ['PATH']


from rbtools.api.client import RBClient
from rbtools.api.errors import APIError, ServerInterfaceError
from rbtools.clients.errors import AmendError
from rbtools.clients.perforce import PerforceClient
from rbtools.hooks.common import initialize_logging
from rbtools.utils.repository import get_repository_resource

# Whether to enable enhanced debug output.
#
# Output will be shown when submitting changes. It will be output to stderr.
DEBUG = False

# The Review Board server URL.
REVIEWBOARD_URL = ''

# The username and password to be supplied to the Review Board server. This
# user must have the "can_change_status" permission granted.
REVIEWBOARD_USERNAME = ''
REVIEWBOARD_PASSWORD = ''
REVIEWBOARD_API_TOKEN = ''

# Configure the Perforce credentials and port.
#
# You will to configure a standard user, not an operator or service user.
#
# These can also be set as command line options (--p4-user, --p4-passwd,
# --p4-port).
#
# The $P4USER and $P4PORT environment variables are used by default.
P4_USER = ''
P4_PASSWORD = ''
P4_PORT = ''

# An explicit P4 client and matching host to use for any operations.
#
# If a client is not specified, the default one for the user and system will
# be used, as determined by Perforce.
#
# If a client has a "Host:" setting, then P4_HOST should be set to match.
P4_CLIENT = None
P4_HOST = None

# If using SSL, configure a path to a pre-populated p4trust file. This can
# be generated on any system.
P4_TRUST_FILE = None

# Whether to close review requests after being submitted successfully.
CLOSE_REVIEW_REQUESTS = True

# Whether to require the changeset to have a review request.
REQUIRE_REVIEW_REQUESTS = True

# Whether to decline changes that aren't approved.
REQUIRE_APPROVAL = True

# Whether to stamp the change with the review request URL.
STAMP_CHANGES = True

# The prefix for the review request field being added to the description.
REVIEW_REQUEST_URL_FIELD = 'Reviewed at:'


def main():
    arg_parser = argparse.ArgumentParser()

    # Debugging options.
    group = arg_parser.add_argument_group('Debugging Options')
    group.add_argument(
        '--debug',
        action='store_true',
        default=DEBUG,
        help='Enable debug output.')
    group.add_argument(
        '--no-debug',
        action='store_false',
        dest='debug',
        default=DEBUG,
        help='Disable debug output.')

    # Review Board connection options.
    group = arg_parser.add_argument_group('Review Board Options')
    group.add_argument(
        '--server',
        dest='rb_server',
        default=REVIEWBOARD_URL,
        help='The URL to the Review Board server.')
    group.add_argument(
        '--username',
        dest='rb_username',
        default=REVIEWBOARD_USERNAME,
        help=(
            'The Review Board user used to verify and close review requests. '
            'This user must have the can_change_status permission granted.'
        ))
    group.add_argument(
        '--api-token',
        dest='rb_api_token',
        default=REVIEWBOARD_API_TOKEN,
        help='The API token for the Review Board user.')
    group.add_argument(
        '--password',
        dest='rb_password',
        default=REVIEWBOARD_PASSWORD,
        help=(
            'The password for the Review Board user, if not using API tokens.'
        ))
    group.add_argument(
        '--disable-ssl-verification',
        dest='disable_ssl_verification',
        action='store_true',
        default=False,
        help='Disable SSL certificate verification.')

    # Perforce connection options.
    group = arg_parser.add_argument_group('Perforce Options')
    group.add_argument(
        '--p4-user',
        dest='p4_user',
        default=P4_USER,
        help=(
            "The Perforce user used to stamp changes. Pass `%%user%%` to use "
            "the submitting user's username."
        ))
    group.add_argument(
        '--p4-passwd',
        dest='p4_passwd',
        default=P4_PASSWORD,
        help='The password used for the user.')
    group.add_argument(
        '--p4-client',
        dest='p4_client',
        default=P4_CLIENT,
        help=(
            "The Perforce client used to stamp changes. Pass `%%client%%` to "
            "use the submitting user's client."
        ))
    group.add_argument(
        '--p4-host',
        dest='p4_host',
        default=P4_HOST,
        help=(
            "The Perforce host matching the client. Pass `%%clienthost%%` to "
            "use the submitting user's host."
        ))
    group.add_argument(
        '--p4-port',
        dest='p4_port',
        default=P4_PORT or os.environ.get('P4PORT'),
        help='The Perforce server to connect to.')
    group.add_argument(
        '--p4-trust-file',
        dest='p4_trust_file',
        default=P4_TRUST_FILE,
        help='A Perforce trust file used for verifying server connections.')

    # Trigger action options.
    group = arg_parser.add_argument_group('Trigger Actions')
    group.add_argument(
        '--close-review-requests',
        action='store_true',
        dest='close_review_requests',
        default=CLOSE_REVIEW_REQUESTS,
        help=(
            'Close review requests once submitted. Overrides the default '
            'in the trigger script.'
        ))
    group.add_argument(
        '--no-close-review-requests',
        action='store_false',
        dest='close_review_requests',
        help=(
            "Leave review requests open once submitted. Overrides the "
            "default in the trigger script."
        ))
    group.add_argument(
        '--require-review-requests',
        action='store_true',
        dest='require_review_requests',
        default=REQUIRE_REVIEW_REQUESTS,
        help=(
            'Require a matching review request. Overrides the default in '
            'the trigger script.'
        ))
    group.add_argument(
        '--no-require-review-requests',
        action='store_false',
        dest='require_review_requests',
        help=(
            "Don't requiring a matching review request. Overrides the "
            "default in the trigger script."
        ))
    group.add_argument(
        '--require-approval',
        action='store_true',
        dest='require_approval',
        default=REQUIRE_APPROVAL,
        help=(
            'Require approval on the review request. Overrides the default '
            'in the trigger script.'
        ))
    group.add_argument(
        '--no-require-approval',
        action='store_false',
        dest='require_approval',
        help=(
            "Don't require approval on the review request. Overrides the "
            "default in the trigger script."
        ))
    group.add_argument(
        '--stamp',
        action='store_true',
        dest='stamp',
        default=STAMP_CHANGES,
        help=(
            'Stamp the review request URL onto a change description. '
            'Overrides the default in the trigger script.'
        ))
    group.add_argument(
        '--no-stamp',
        action='store_false',
        dest='stamp',
        help=(
            "Don't stamp the review request URL onto a change description. "
            "Overrides the default in the trigger script."
        ))

    # Positional arguments.
    arg_parser.add_argument(
        'changenum',
        type=int,
        nargs=1,
        help='The submitted change number.')

    # Parse the options and validate them.
    options = arg_parser.parse_args()
    print(options)

    p4_client = options.p4_client
    p4_host = options.p4_host
    p4_passwd = options.p4_passwd
    p4_port = options.p4_port
    p4_trust_file = options.p4_trust_file
    p4_user = options.p4_user
    rb_api_token = options.rb_api_token
    rb_password = options.rb_password
    rb_url = options.rb_server
    rb_username = options.rb_username

    if not rb_url:
        sys.stderr.write('--server is required.\n')
        sys.exit(1)

    if not rb_username:
        sys.stderr.write('--username is required.\n')
        sys.exit(1)

    if not rb_api_token and not rb_password:
        sys.stderr.write('--api-token or --password is required.\n')
        sys.exit(1)

    if not p4_port:
        sys.stderr.write('--p4-port or $P4PORT is required.\n')
        sys.exit(1)

    if not p4_user:
        sys.stderr.write('--p4-user or $P4USER is required.\n')
        sys.exit(1)

    # Set up logging.
    initialize_logging(debug=options.debug)

    # Set up the Perforce environment.
    os.environ.update({
        'P4USER': p4_user,
        'P4PORT': p4_port,
    })

    if p4_client:
        os.environ['P4CLIENT'] = p4_client

    if p4_passwd:
        os.environ['P4PASSWD'] = p4_passwd

    if p4_host:
        os.environ['P4HOST'] = p4_host

    if p4_trust_file:
        os.environ['P4TRUST'] = p4_trust_file

    # Get the changeset from Perforce.
    changenum = options.changenum[0]
    assert isinstance(changenum, int)

    client = PerforceClient()
    changes = client.p4.change(changenum)

    # Connect to Review Board.
    api_client = RBClient(url=rb_url,
                          username=rb_username,
                          password=rb_password,
                          api_token=rb_api_token,
                          in_memory_cache=True,
                          verify_ssl=not options.disable_ssl_verification)

    try:
        api_root = api_client.get_root()
    except ServerInterfaceError as e:
        sys.stderr.write('Could not reach the Review Board server at %s: %s\n'
                         % (rb_url, e))
        sys.exit(1)
    except APIError as e:
        sys.stderr.write('Unexpected API error when talking to Review Board '
                         'at %s: %s'
                         % (rb_url, e))
        sys.exit(1)

    # Find the review request for this changenum.
    review_request = None

    if changes and len(changes) == 1:
        # Look up the repository for this server.
        try:
            repository = get_repository_resource(
                api_root=api_root,
                repository_paths=[p4_port])[0]
        except APIError as e:
            # 100 = Does Not Exist.
            if e.error_code == 100:
                repository = None
            else:
                sys.stderr.write(
                    'Error looking up a repository in Review Board for '
                    '%s: %s\n'
                    % (p4_port, e))

            sys.exit(1)

        if repository is None:
            sys.stderr.write(
                'Could not find a repository in Review Board for %s.\n'
                % p4_port)
            sys.exit(1)

        # Look up a review request for this changenum.
        try:
            review_requests = api_root.get_review_requests(
                commit_id=changenum,
                repository=repository.id,
                only_fields='approved,approval_failure,id')
        except APIError as e:
            sys.stderr.write(
                'Unexpected error looking up a matching review request: %s\n'
                % e)
            sys.exit(1)

        if len(review_requests) == 1:
            review_request = review_requests[0]
        elif len(review_requests) >= 1:
            sys.stderr.write(
                'More than one review request was found for changenum %s and '
                'P4PORT %s. This may be an internal error. Please contact '
                'your Perforce administrator.\n'
                % (changenum, p4_port))
            sys.exit(1)

    if review_request:
        # A review request ID was found.
        #
        # We'll now stamp changes if the script is configured to enable this,
        # whether or not the change is approved.
        if options.stamp:
            # Update the changeset if needed in order to include the review
            # request URL.
            assert changes is not None
            change_description = changes[0]['Description']

            stamp_str = '%s %s' % (REVIEW_REQUEST_URL_FIELD,
                                   review_request.absolute_url)

            if stamp_str not in change_description:
                try:
                    client.amend_commit_description(
                        '%s\n\n%s' % (change_description.rstrip(), stamp_str),
                        revisions=client.parse_revision_spec([str(changenum)]))
                except AmendError as e:
                    sys.stderr.write(
                        'Unable to amend change %s with the review request '
                        'URL: %s\n'
                        % (changenum, e))
                    sys.exit(1)

        if not review_request.approved:
            # The review request was not approved in Review Board. Display
            # an error.
            if options.require_approval:
                sys.stderr.write('%s\n' % review_request.approval_failure)
                sys.exit(1)
            else:
                logging.warning(
                    'Change #%s was not approved, but is allowed to be '
                    'submitted (%s)',
                    changenum, review_request.approval_failure)

        # If we're here, the change is allowed to go into Review Board.
        if options.close_review_requests:
            # Close the review request, and point to this change.
            review_request.update(
                status='submitted',
                description='Submitted as change #%s' % changenum)
    else:
        # A review request ID was not found.
        if options.require_review_requests:
            sys.stderr.write(
                'A review request for this change must be posted for review '
                'and approved before it can be submitted.\n')
            sys.exit(1)
        else:
            logging.warning(
                "Change #%s hasn't been posted for review, but is allowed to "
                "be submitted.",
                changenum)


if __name__ == '__main__':
    main()
