import json
import logging
import os
import time
import urllib2
from argparse import ArgumentParser

logging.basicConfig(format='%(asctime)s [%(levelname)s] %(message)s', level=logging.DEBUG)


class QloudClient(object):
    _QLOUD_BASE_URL = "https://qloud-ext.yandex-team.ru"
    _QLOUD_ENVIRONMENT_INFO_HANDLE = "/api/v1/environment/dump/{environmentId}"
    _QLOUD_ENVIRONMENT_INFO_BY_VERSION_HANDLE = "/api/v1/environment/dump/{environmentId}/{version}"

    _QLOUD_ENVIRONMENT_CURRENT_HANDLE = "/api/v1/environment/current/{environmentId}"
    _QLOUD_SUGGEST_HANDLE = "/api/docker/hash?registryUrl={registry_url}&tag={tag}"
    _QLOUD_FAST_DEPLOY_HANDLE = "/api/v1/environment/fast-deploy/{environmentId}/{version}"

    _QLOUD_UPLOAD_DRY_RUN_HANDLE = "/api/v1/environment/upload/dry-run"
    _QLOUD_DEPLOY_HANDLE = "/api/v1/environment/upload/return-header?targetState={targetState}&skipIfNoChange={skipIfNoChange}"

    _QLOUD_DEPLOY_PING_INITIAL_DELAY = 100
    _QLOUD_DEPLOY_PING_PERIOD = 30

    _REQUEST_RETRY_COUNT = 2

    class Error(Exception):
        def __init__(self, msg):
            self._msg = msg

        def __str__(self):
            return self._msg

    def __init__(self, oauth_token):
        self._oauth_token = oauth_token

    def environment_info(self, environment_id, version=None):
        if version is None:
            return self._request(QloudClient._QLOUD_ENVIRONMENT_INFO_HANDLE.format(environmentId=environment_id))
        return self._request(QloudClient._QLOUD_ENVIRONMENT_INFO_BY_VERSION_HANDLE.format(environmentId=environment_id,
                                                                                          version=version))

    def current_evironment(self, environment_id):
        return self._request(QloudClient._QLOUD_ENVIRONMENT_CURRENT_HANDLE.format(environmentId=environment_id))

    def suggest(self, registry_url, tag):
        return self._request(QloudClient._QLOUD_SUGGEST_HANDLE.format(registry_url=registry_url, tag=tag), parse_to_json=False)

    def version(self):
        return self._request(QloudClient)

    def fast_deploy(self, environment_id, version, update_data):
        return self._request(QloudClient._QLOUD_FAST_DEPLOY_HANDLE.format(environmentId=environment_id, version=version), update_data,
                             parse_to_json=False)

    def deploy(self, update_data, target_state="DEPLOYED", skip_if_no_change=False):
        return self._request(QloudClient._QLOUD_DEPLOY_HANDLE.format(targetState=target_state, skipIfNoChange=skip_if_no_change),
                             update_data)

    def wait_deployment(self, environment_id, version):
        time.sleep(QloudClient._QLOUD_DEPLOY_PING_INITIAL_DELAY)
        while True:
            status = self.environment_info(environment_id, version)["status"]
            logging.debug("Current status of artifact %s deployment is %s" % (version, status))
            if status == "FAILED":
                raise RuntimeError("Failed to deploy artifact '%s'!" % version)
            if status == "CANCELED":
                raise RuntimeError("Canceled deploying artifact '%s'" % version)
            if status == "REMOVED":
                raise RuntimeError("Removed deploying artifact '%s'" % version)
            if status == "DEPLOYED":
                break
            time.sleep(QloudClient._QLOUD_DEPLOY_PING_PERIOD)

    def _request(self, url_path, data=None, parse_to_json=True):
        request = urllib2.Request(QloudClient._QLOUD_BASE_URL + url_path)
        request.add_header("Authorization", "OAuth %s" % self._oauth_token)
        if data is not None:
            request.add_header("Content-Type", "application/json")
            request.add_data(json.dumps(data))
        logging.debug("Sending request: [{method}, {url}, {headers}, {data}]"
                      .format(method=request.get_method(), url=request.get_full_url(),
                              headers=map(mask_authorization_header, request.header_items()),
                              data=request.get_data()))
        retry_count = 0
        while True:
            try:
                response = urllib2.urlopen(request)
                logging.debug("Received response: %s" % response.getcode())
                response_body = response.read()
                if parse_to_json:
                    return json.loads(response_body)
                return response_body
            except urllib2.HTTPError as error:
                error_message = "{status}, {reason}, {body}".format(status=error.getcode(), reason=error.reason, body=error.read())
                logging.error("Error while processing request: " + error_message)
                if self._should_retry(error, retry_count):
                    retry_count += 1
                else:
                    raise QloudClient.Error("Request to Qloud failed: " + error_message)

    @staticmethod
    def _should_retry(http_error, retry_count):
        return http_error.getcode() == 500 and retry_count < QloudClient._REQUEST_RETRY_COUNT


def mask_authorization_header(header):
    if header[0] == "Authorization":
        return header[0], "***"
    return header


def extract_component_data(component, deploying_artifact_id, client):
    component_type = component["componentType"]

    if component_type == "qe-backend" or component_type == "qe-frontend":
        deployed_artifact_id = component["artifactFqn"]
        if deployed_artifact_id == deploying_artifact_id:
            print "Deployment skipped: artifact %s already deployed" % deploying_artifact_id
            return None
        return {
            "artifactFqn": deploying_artifact_id
        }

    if component_type == "shooting-ground" or component_type == "standard":
        deploying_artifact_version = deploying_artifact_id[deploying_artifact_id.rfind(":") + 1:].replace('-', '_')

        deployed_repository = component['properties']["repository"]

        repository_url = deployed_repository[:deployed_repository.rfind(":")]

        deploying_repository = repository_url + ":" + deploying_artifact_version

        deploying_hash = client.suggest(repository_url, deploying_artifact_version)
        deployed_hash = component['properties']['hash']

        if deployed_repository == deploying_repository and deployed_hash == deploying_hash:
            print "Deployment skipped: repository %s with hash %s already deployed" % (deploying_repository, deploying_hash)
            return None

        return {
            "repository": deploying_repository,
            "hash": deploying_hash
        }

    raise AttributeError("Unknown component type '" + component_type + "'!")


def main():
    args_parser = ArgumentParser()
    args_parser.add_argument("--environment-id", help="", required=True)
    args_parser.add_argument("--component-names", help="", required=True)
    args_parser.add_argument("--artifact-id", help="", required=True)
    args_parser.add_argument("--oauth-token", help="", required=True)
    args_parser.add_argument("--wait", help="", required=False)
    args_parser.add_argument("--target-state", help="", default="DEPLOYED", required=False)
    args = args_parser.parse_args()

    check_args(args)

    client = QloudClient(args.oauth_token)
    environment_id = args.environment_id
    logging.info("Requesting information for environment '%s'" % environment_id)

    current_environment = client.current_evironment(environment_id)
    environment_version = current_environment["version"]

    environment_info = client.environment_info(environment_id, environment_version)

    component_names = args.component_names.split(",")

    has_update = False

    for component in environment_info["components"]:
        if component['componentName'] in component_names:
            component_data = extract_component_data(component, args.artifact_id, client)

            if component_data is not None:
                has_update = True

                component['properties']['hash'] = component_data['hash']
                component['properties']['repository'] = component_data['repository']

    if not has_update:
        return

    logging.info("Deploing")

    tickets_string = ''
    tickets_path = 'target/tickets.txt'
    if os.path.isfile(tickets_path):
        with open(tickets_path) as f:
            tickets_string = '\n\n' + f.read()

    artifact_id_parts = args.artifact_id.split(":")
    environment_info['comment'] = artifact_id_parts[2] + ". Upgrade by dispenser qloud script." + tickets_string

    client.deploy(environment_info, target_state=args.target_state)

    if args.wait == 'True':
        client.wait_deployment(environment_id, deploying_version)


def check_args(args):
    environment_id_parts = args.environment_id.split(".")
    if len(environment_id_parts) != 3 or has_empty_element(environment_id_parts):
        raise RuntimeError("Incorrect environment id: " + args.environment_id)
    artifact_id_parts = args.artifact_id.split(":")
    if len(artifact_id_parts) != 3 or has_empty_element(artifact_id_parts):
        raise RuntimeError("Incorrect artifact id: " + args.artifact_id)


def has_empty_element(sequence):
    return reduce(lambda has_empty, s: has_empty or not s, sequence, False)


if __name__ == '__main__':
    main()
