# Copyright (C) 2018-2022 Apple Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1.  Redistributions of source code must retain the above copyright
#     notice, this list of conditions and the following disclaimer.
# 2.  Redistributions in binary form must reproduce the above copyright
#     notice, this list of conditions and the following disclaimer in the
#     documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import logging
import os
import re
import requests
import subprocess

import ews.common.util as util
import ews.config as config

_log = logging.getLogger(__name__)


class Buildbot():
    # Buildbot status codes referenced from https://github.com/buildbot/buildbot/blob/master/master/buildbot/process/results.py
    ALL_RESULTS = list(range(7))
    SUCCESS, WARNINGS, FAILURE, SKIPPED, EXCEPTION, RETRY, CANCELLED = ALL_RESULTS
    icons_for_queues_mapping = {}
    queue_name_by_shortname_mapping = {}
    builder_name_to_id_mapping = {}

    # FIXME: Auto-generate the queue's trigger relationship
    QUEUE_TRIGGERS = {
        'api-ios': 'ios-sim',
        'ios-wk2': 'ios-sim',
        'ios-wk2-wpt': 'ios-sim',
        'api-mac': 'mac',
        'mac-wk1': 'mac',
        'mac-wk2': 'mac',
        'mac-wk2-stress': 'mac',
        'mac-debug-wk1': 'mac-debug',
        'mac-AS-debug-wk2': 'mac-AS-debug',
        'api-gtk': 'gtk',
        'gtk-wk2': 'gtk',
        'wpe-wk2': 'wpe',
        'jsc-mips-tests': 'jsc-mips',
        'jsc-armv7-tests': 'jsc-armv7',
    }

    @classmethod
    def send_patch_to_buildbot(cls, patch_path, send_to_commit_queue=False, properties=None):
        properties = properties or []
        buildbot_port = config.COMMIT_QUEUE_PORT if send_to_commit_queue else config.BUILDBOT_SERVER_PORT
        command = ['buildbot', 'try',
                   '--connect=pb',
                   '--master={}:{}'.format(config.BUILDBOT_TRY_HOST, buildbot_port),
                   '--username={}'.format(config.BUILDBOT_TRY_USERNAME),
                   '--passwd={}'.format(config.BUILDBOT_TRY_PASSWORD),
                   '--diff={}'.format(patch_path),
                   '--repository=']

        for property in properties:
            command.append('--property={}'.format(property))

        return_code = subprocess.call(command)
        if return_code:
            _log.warn('Error executing buildbot try command for {}, return code={}'.format(patch_path, return_code))

        return return_code

    @classmethod
    def get_builder_id_to_name_mapping(cls):
        builder_id_to_name_mapping = {}
        builder_url = 'http://{}/api/v2/builders'.format(config.BUILDBOT_SERVER_HOST)
        builders_data = util.fetch_data_from_url(builder_url)
        if not builders_data:
            return {}
        for builder in builders_data.json().get('builders', []):
            builder_id = builder['builderid']
            builder_name = builder.get('name')
            display_name = builder.get('description')
            if not display_name:
                display_name = Buildbot._get_display_name_from_builder_name(builder_name)
            builder_id_to_name_mapping[builder_id] = {'builder_name': builder_name, 'display_name': display_name}
        return builder_id_to_name_mapping

    @classmethod
    def _get_display_name_from_builder_name(cls, builder_name):
        words = re.split(r'[, \-_:()]+', builder_name)
        if not words:
            return builder_name
        return words[0].lower()

    @classmethod
    def fetch_config(cls):
        config_url = 'https://{}/config.json'.format(config.BUILDBOT_SERVER_HOST)
        config_data = util.fetch_data_from_url(config_url)
        if not config_data:
            return {}
        try:
            return config_data.json()
        except Exception as e:
            _log.error('Error in fetching {}. Error: {}'.format(config_url, e))
            return {}

    @classmethod
    def update_icons_for_queues_mapping(cls):
        config = cls.fetch_config()
        if not config:
            _log.error('Unable to fetch buildbot config.json')
            return
        for builder in config.get('builders', []):
            shortname = builder.get('shortname')
            Buildbot.icons_for_queues_mapping[shortname] = builder.get('icon')
            Buildbot.queue_name_by_shortname_mapping[shortname] = builder.get('name')

    @classmethod
    def update_builder_name_to_id_mapping(cls):
        url = 'https://{}/api/v2/builders'.format(config.BUILDBOT_SERVER_HOST)
        builders_data = util.fetch_data_from_url(url)
        if not builders_data:
            return
        for builder in builders_data.json().get('builders', []):
            name = builder.get('name')
            Buildbot.builder_name_to_id_mapping[name] = builder.get('builderid')

    @classmethod
    def fetch_pending_and_inprogress_builds(cls, builder_full_name):
        builderid = Buildbot.builder_name_to_id_mapping.get(builder_full_name)
        if not Buildbot.builder_name_to_id_mapping:
            _log.warn('Missing builder_name_to_id_mapping, refetching it from {}'.format(config.BUILDBOT_SERVER_HOST))
            cls.update_builder_name_to_id_mapping()

        if not builderid:
            _log.error('Invalid builder: {}. Number of builders: {}'.format(builder_full_name, len(cls.builder_name_to_id_mapping)))
            return {}
        url = 'https://{}/api/v2/builders/{}/buildrequests?complete=false&property=*'.format(config.BUILDBOT_SERVER_HOST, builderid)
        builders_data = util.fetch_data_from_url(url)
        if not builders_data:
            return {}
        return builders_data.json()

    @classmethod
    def get_patches_in_queue(cls, builder_full_name):
        patch_ids = []
        builds = cls.fetch_pending_and_inprogress_builds(builder_full_name)
        for buildrequest in builds.get('buildrequests', []):
            properties = buildrequest.get('properties')
            if properties:
                patch_ids.append(properties.get('patch_id')[0])
        _log.debug('Patches in queue for {}: {}'.format(builder_full_name, patch_ids))
        return patch_ids

    @classmethod
    def retry_build(cls, builder_id, build_number):
        if not (util.is_valid_id(builder_id) and util.is_valid_id(build_number)):
            return False

        build_url = 'https://{}/api/v2/builders/{}/builds/{}'.format(config.BUILDBOT_SERVER_HOST, builder_id, build_number)
        username = os.getenv('EWS_ADMIN_USERNAME')
        password = os.getenv('EWS_ADMIN_PASSWORD')
        session = requests.Session()
        response = session.head('https://{}/auth/login'.format(config.BUILDBOT_SERVER_HOST), auth=(username, password))
        if (not response) or response.status_code not in (200, 302):
            _log.error('Authentication to {} failed. Please check username/password.'.format(config.BUILDBOT_SERVER_HOST))
            return False

        json_data = {'method': 'rebuild', 'id': 1, 'jsonrpc': '2.0', 'params': {'reason': 'retried-by-user'}}
        response = session.post(build_url, json=json_data)

        if response and response.status_code == 200:
            _log.info('Successfuly submitted retry request for build: {}'.format(build_url))
            return True

        _log.error('Failed to retry build: {}, http response code: {}'.format(build_url, response.status_code))
        return False

    @classmethod
    def is_tester_queue(self, queue):
        icon = Buildbot.icons_for_queues_mapping.get(queue)
        return icon in ['testOnly', 'buildAndTest']

    @classmethod
    def is_builder_queue(self, queue):
        icon = Buildbot.icons_for_queues_mapping.get(queue)
        return icon in ['buildOnly', 'buildAndTest']

    @classmethod
    def get_parent_queue(self, queue):
        return Buildbot.QUEUE_TRIGGERS.get(queue)
