# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.

import os
import random
import time
import warnings

import botocore
import botocore.exceptions
import botocore.parsers
import botocore.session
from botocore.config import Config
from botocore.loaders import Loader
from cement.utils.misc import minimal_logger

from ebcli import __version__
from ebcli.core import fileoperations
from ebcli.lib.botopatch import apply_patches
from ebcli.lib.utils import static_var
from ebcli.objects.exceptions import ServiceError, NotAuthorizedError, \
    CredentialsError, NoRegionError,  ValidationError, \
    InvalidProfileError, ConnectionError, AlreadyExistsError, NotFoundError, \
    NotAuthorizedInRegionError
from ebcli.resources.strings import strings

LOG = minimal_logger(__name__)

BOTOCORE_DATA_FOLDER_NAME = 'botocoredata'

_api_clients = {}
_profile = None
_profile_env_var = 'AWS_EB_PROFILE'
_id = None
_key = None
_region_name = None
_verify_ssl = True
_endpoint_url = None
_debug = False

apply_patches()


def _flush():
    # Should be used for resetting tests only
    global _api_clients, _profile, _id, _key, _region_name, _verify_ssl
    _api_clients = {}
    _get_botocore_session.botocore_session = None
    _profile = None
    _id = None
    _key = None
    _region_name = None
    _verify_ssl = True


def set_session_creds(id, key):
    global _api_clients, _id, _key
    _id = id
    _key = key

    # invalidate all old clients
    _api_clients = {}


def set_profile(profile):
    global _profile, _api_clients
    _profile = profile

    # Invalidate session and old clients
    _get_botocore_session.botocore_session = None
    _api_clients = {}


def get_profile():
    if _profile is not None:
        return _profile
    from ebcli.operations import commonops
    return commonops.get_default_profile(require_default=True)


def set_region(region_name):
    global _region_name
    _region_name = region_name

    # Invalidate session and old clients
    _get_botocore_session.botocore_session = None


def set_endpoint_url(endpoint_url):
    global _endpoint_url
    _endpoint_url = endpoint_url


def no_verify_ssl():
    global _verify_ssl
    _verify_ssl = False


def set_profile_override(profile):
    global _profile_env_var
    set_profile(profile)
    _profile_env_var = None


def set_debug():
    global _debug
    _debug = True


def _set_user_agent_for_session(session):
    session.user_agent_name = 'eb-cli'
    session.user_agent_version = __version__


def _get_data_loader():
    # Creates a botocore data loader that loads custom data files
    # FIRST, creating a precedence for custom files.
    data_folder = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                               BOTOCORE_DATA_FOLDER_NAME)

    return Loader(extra_search_paths=[data_folder, Loader.BUILTIN_DATA_PATH],
                  include_default_search_paths=False)


def _get_client(service_name):
    aws_access_key_id = _id
    aws_secret_key = _key
    if service_name in _api_clients:
        return _api_clients[service_name]

    session = _get_botocore_session()
    if service_name == 'elasticbeanstalk':
        endpoint_url = _endpoint_url
    else:
        endpoint_url = None
    try:
        LOG.debug('Creating new Botocore Client for ' + str(service_name))
        client = session.create_client(service_name,
                                       endpoint_url=endpoint_url,
                                       aws_access_key_id=aws_access_key_id,
                                       aws_secret_access_key=aws_secret_key,
                                       verify=_verify_ssl,
                                       config=Config(signature_version='s3v4'))

    except botocore.exceptions.ProfileNotFound as e:
        raise InvalidProfileError(e)
    LOG.debug('Successfully created session for ' + service_name)

    _api_clients[service_name] = client
    return client


@static_var('botocore_session', None)
def _get_botocore_session():
    if _get_botocore_session.botocore_session is None:
        LOG.debug('Creating new Botocore Session')
        LOG.debug('Botocore version: {0}'.format(botocore.__version__))
        session = botocore.session.get_session({
            'profile': (None, _profile_env_var, _profile, None),
        })
        session.set_config_variable('region', _region_name)
        session.set_config_variable('profile', _profile)
        session.register_component('data_loader', _get_data_loader())
        _set_user_agent_for_session(session)
        _get_botocore_session.botocore_session = session
        if _debug:
            session.set_debug_logger()

    return _get_botocore_session.botocore_session


def get_region_name():
    return _region_name


def get_credentials():
    client_creds = _get_client('elasticbeanstalk')._request_signer._credentials
    return botocore.credentials.Credentials(
        access_key=client_creds.access_key,
        secret_key=client_creds.secret_key,
    )


def make_api_call(service_name, operation_name, **operation_options):
    operation = _set_operation(service_name, operation_name)
    aggregated_error_message = []
    max_attempts = 10

    region = _region_name
    if not region:
        region = 'default'
    if region == 'placeholder':
        set_region(fileoperations.get_config_setting('global', 'default_region'))

    attempt = 0
    while True:
        attempt += 1
        if attempt > 1:
            LOG.debug('Retrying -- attempt #' + str(attempt))
        _sleep(_get_delay(attempt))
        try:
            LOG.debug('Making api call: (' +
                      service_name + ', ' + operation_name +
                      ') to region: ' + region + ' with args:' + str(operation_options))
            response_data = operation(**operation_options)
            status = response_data['ResponseMetadata']['HTTPStatusCode']
            LOG.debug('API call finished, status = ' + str(status))
            if response_data:
                LOG.debug('Response: ' + str(response_data))

            return response_data

        except botocore.exceptions.ClientError as e:
            _handle_response_code(e.response, attempt, aggregated_error_message)
        except botocore.parsers.ResponseParserError as e:
            LOG.debug('Botocore could not parse response received')
            if attempt > max_attempts:
                raise MaxRetriesError(
                    'Max retries exceeded for ResponseParserErrors'
                    + os.linesep.join(aggregated_error_message)
                )

            aggregated_error_message.insert(attempt, str(e))
        except botocore.exceptions.NoCredentialsError:
            LOG.debug('No credentials found')
            raise CredentialsError('Operation Denied. You appear to have no'
                                   ' credentials')
        except botocore.exceptions.PartialCredentialsError as e:
            LOG.debug('Credentials incomplete')
            raise CredentialsError(str(e))

        except (botocore.exceptions.ValidationError,
                botocore.exceptions.ParamValidationError) as e:
            raise ValidationError(str(e))

        except botocore.exceptions.EndpointConnectionError as e:
            LOG.debug(e)
            raise

        except botocore.exceptions.BotoCoreError:
            LOG.error('Botocore Error')
            raise

        except IOError as error:
            if hasattr(error.args[0], 'reason') and str(error.args[0].reason) == \
                    '[Errno -2] Name or service not known':
                raise ConnectionError()

            LOG.error('Error while contacting Elastic Beanstalk Service')
            LOG.debug('error:' + str(error))
            raise ServiceError(error)


def _handle_response_code(response_data, attempt, aggregated_error_message):
    max_attempts = 10

    LOG.debug('Response: ' + str(response_data))
    status = response_data['ResponseMetadata']['HTTPStatusCode']
    LOG.debug('API call finished, status = ' + str(status))
    try:
        message = str(response_data['Error']['Message'])
    except KeyError:
        message = ""
    if status == 400:
        error = _get_400_error(response_data, message)
        if isinstance(error, ThrottlingError):
            LOG.debug('Received throttling error')
            if attempt > max_attempts:
                raise MaxRetriesError('Max retries exceeded for '
                                      'throttling error')
        else:
            raise error
    elif status == 403:
        LOG.debug('Received a 403')
        if not message:
            message = 'Are your permissions correct?'
        if _region_name == 'cn-north-1':
            raise NotAuthorizedInRegionError('Operation Denied. ' + message +
                                             '\n' +
                                             strings['region.china.credentials'])
        else:
            raise NotAuthorizedError('Operation Denied. ' + message)
    elif status == 404:
        LOG.debug('Received a 404')
        raise NotFoundError(message)
    elif status == 409:
        LOG.debug('Received a 409')
        raise AlreadyExistsError(message)
    elif status in (500, 503, 504):
        LOG.debug('Received 5XX error')
        retry_failure_message = \
            'Received 5XX error during attempt #{0}\n   {1}\n'.format(
                str(attempt),
                message
            )

        aggregated_error_message.insert(attempt, retry_failure_message)

        if attempt > max_attempts:
            _handle_500_error(('\n').join(aggregated_error_message))
    else:
        raise ServiceError('API Call unsuccessful. '
                           'Status code returned ' + str(status))


def _set_operation(service_name, operation_name):
    try:
        client = _get_client(service_name)
    except botocore.exceptions.UnknownEndpointError as e:
        raise NoRegionError(e)
    except botocore.exceptions.PartialCredentialsError as e:
        LOG.debug('Credentials incomplete')
        raise CredentialsError('Your credentials are not complete. Error: {0}'
                               .format(e))
    except botocore.exceptions.NoRegionError:
        raise NoRegionError()

    if not _verify_ssl:
        warnings.filterwarnings("ignore")

    return getattr(client, operation_name)


def _get_delay(attempt_number):
    if attempt_number == 1:
        return 0
    # Exponential backoff
    rand_int = random.randrange(0, 2**attempt_number)
    delay = rand_int * 0.05  # delay time is 50 ms
    LOG.debug('Sleeping for ' + str(delay) + ' seconds.')
    return delay


def _get_400_error(response_data, message):
    code = response_data['Error']['Code']
    LOG.debug('Received a 400 Error')
    if code == 'InvalidParameterValue':
        return InvalidParameterValueError(message)
    elif code == 'InvalidQueryParameter':
        return InvalidQueryParameterError(message)
    elif code.startswith('Throttling'):
        return ThrottlingError(message)
    elif code.startswith('ResourceNotFound'):
        return NotFoundError(message)
    elif code.startswith('TooManyPlatformsException'):
        return TooManyPlatformsError(message)
    elif code.startswith('TooManyConfigurationTemplatesException'):
        message = [
            'Your request cannot be completed. You have reached the maximum',
            'number of saved configuration templates. Learn more about service',
            'limits: http://docs.aws.amazon.com/general/latest/gr/aws_service_limits.html'
        ]
        return TooManyConfigurationTemplatesException(' '.join(message))
    else:
        return ServiceError(message, code=code)


def _handle_500_error(aggregated_error_message):
    raise MaxRetriesError('Max retries exceeded for '
                          'service error (5XX)\n' +
                          aggregated_error_message)


def _sleep(delay):
    time.sleep(delay)


class InvalidParameterValueError(ServiceError):
    pass


class InvalidQueryParameterError(ServiceError):
    pass


class ThrottlingError(ServiceError):
    pass


class MaxRetriesError(ServiceError):
    pass


class TooManyPlatformsError(ServiceError):
    pass


class TooManyConfigurationTemplatesException(ServiceError):
    pass
