"""
    Copyright 2018 EPAM Systems, Inc.

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License 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 re
from datetime import datetime, timedelta, timezone

from botocore.exceptions import ClientError

from syndicate.exceptions import InternalError, ConfigurationError
from syndicate.commons.log_helper import get_logger, get_user_logger
from syndicate.connection import ConnectionProvider
from syndicate.connection.sts_connection import STSConnection
from syndicate.core.conf.processor import ConfigHolder
from syndicate.core.project_state.project_state import ProjectState
from syndicate.core.resources.processors_mapping import ProcessorFacade
from syndicate.core.resources.resources_provider import ResourceProvider
from syndicate.core.conf.bucket_view import URIBucketView, RegexViewDigest, \
    NAMED_S3_URI_PATTERN, S3_PATTERN_GROUP_NAMES
from syndicate.core.helper import handle_interruption

_LOG = get_logger(__name__)
USER_LOG = get_user_logger()

SESSION_TOKEN = 'aws_session_token'
SECRET_KEY = 'aws_secret_access_key'
ACCESS_KEY = 'aws_access_key_id'

CONFIRMATION_ANSWERS = ['y', 'yes']

# if temporary credentials will be expired in less than N minutes,
# syndicate will regenerate temp credentials instead of using old ones
CREDENTIALS_EXPIRATION_THRESHOLD_MIN = 15


def exception_handler(exception_type, exception, traceback):
    print(exception)


# sys.excepthook = exception_handler

# CONF VARS ===================================================================
CONF_PATH = os.environ.get('SDCT_CONF')
CONFIG: ConfigHolder = None
CONN = None
CREDENTIALS = None
RESOURCES_PROVIDER: ResourceProvider = None
PROCESSOR_FACADE: ProcessorFacade = None
PROJECT_STATE: ProjectState = None


def _ready_to_assume():
    return CONFIG.access_role and not CONFIG.use_temp_creds


def _ready_to_use_creds():
    return not CONFIG.access_role and CONFIG.aws_access_key_id and \
           CONFIG.aws_secret_access_key and not CONFIG.use_temp_creds


def _ready_to_use_provided_temp_creds():
    has_temporary_creds_set = CONFIG.temp_aws_access_key_id \
                              and CONFIG.temp_aws_secret_access_key \
                              and CONFIG.temp_aws_session_token \
                              and CONFIG.expiration

    credentials_expired = True
    
    if has_temporary_creds_set:
        credentials_expired = validate_temp_credentials_expiration(
            expiration=CONFIG.expiration
        )
        if credentials_expired and CONFIG.use_temp_creds:
            raise ConfigurationError('Temporary credentials have expired')

    return has_temporary_creds_set and not credentials_expired


def _ready_to_generate_temp_creds():
    return not CONFIG.access_role and CONFIG.use_temp_creds


def initialize_connection():
    global CONFIG
    global CONN
    global CONF_PATH
    global CREDENTIALS
    global RESOURCES_PROVIDER
    global PROCESSOR_FACADE

    regex_digest = RegexViewDigest()
    regex_digest.expression = NAMED_S3_URI_PATTERN
    regex_digest.groups = S3_PATTERN_GROUP_NAMES
    uri_bucket_view = URIBucketView()
    uri_bucket_view.digest = regex_digest

    CONFIG = ConfigHolder(CONF_PATH)
    CONFIG.deploy_target_bucket_view = uri_bucket_view
    sts = STSConnection(CONFIG.region, CONFIG.aws_access_key_id,
                        CONFIG.aws_secret_access_key, CONFIG.aws_session_token)
    try:
        CREDENTIALS = {
            'region': CONFIG.region
        }
        if _ready_to_use_provided_temp_creds():
            _LOG.debug(f'Going to use previously generated temporary '
                       f'credentials')
            CREDENTIALS[ACCESS_KEY] = CONFIG.temp_aws_access_key_id
            CREDENTIALS[SECRET_KEY] = CONFIG.temp_aws_secret_access_key
            CREDENTIALS[SESSION_TOKEN] = CONFIG.temp_aws_session_token
        elif _ready_to_assume():
            _LOG.debug('Starting to assume role ...')
            # get CREDENTIALS for N hours

            token_code = prompt_mfa_code() if CONFIG.serial_number else None
            temp_credentials = sts.get_temp_credentials(
                role_arn=CONFIG.access_role,
                acc_id=CONFIG.account_id,
                duration=CONFIG.session_duration,
                serial_number=CONFIG.serial_number,
                token_code=token_code
            )
            _LOG.debug(f'Role {CONFIG.access_role} is assumed successfully'
                       f'for {CONFIG.session_duration} seconds')
            CREDENTIALS[ACCESS_KEY] = temp_credentials['AccessKeyId']
            CREDENTIALS[SECRET_KEY] = temp_credentials['SecretAccessKey']
            CREDENTIALS[SESSION_TOKEN] = temp_credentials['SessionToken']
            _LOG.debug(f'Temporary credentials have been successfully '
                       f'generated by assuming the role {CONFIG.access_role}, '
                       f'saving to config.')
            CONFIG.set_temp_credentials_to_config(
                temp_aws_access_key_id=temp_credentials['AccessKeyId'],
                temp_aws_secret_access_key=temp_credentials['SecretAccessKey'],
                temp_aws_session_token=temp_credentials['SessionToken'],
                expiration=temp_credentials['Expiration']
            )
        elif _ready_to_generate_temp_creds():
            _LOG.debug(f'Going to generate new temporary credentials')

            token_code = None
            if CONFIG.serial_number:
                token_code = prompt_mfa_code()
            temp_credentials = sts.get_session_token(
                duration=CONFIG.session_duration,
                serial_number=CONFIG.serial_number,
                token_code=token_code
            )
            CREDENTIALS[ACCESS_KEY] = temp_credentials['AccessKeyId']
            CREDENTIALS[SECRET_KEY] = temp_credentials['SecretAccessKey']
            CREDENTIALS[SESSION_TOKEN] = temp_credentials['SessionToken']
            _LOG.debug(f'Temporary credentials have been successfully '
                       f'generated, saving to config.')
            CONFIG.set_temp_credentials_to_config(
                temp_aws_access_key_id=temp_credentials['AccessKeyId'],
                temp_aws_secret_access_key=temp_credentials['SecretAccessKey'],
                temp_aws_session_token=temp_credentials['SessionToken'],
                expiration=temp_credentials['Expiration']
            )
        elif _ready_to_use_creds():
            _LOG.debug('Credentials access')
            CREDENTIALS[ACCESS_KEY] = CONFIG.aws_access_key_id
            CREDENTIALS[SECRET_KEY] = CONFIG.aws_secret_access_key
        CONN = ConnectionProvider(CREDENTIALS)
        RESOURCES_PROVIDER = ResourceProvider(config=CONFIG,
                                              credentials=CREDENTIALS,
                                              sts_conn=sts)
        PROCESSOR_FACADE = ProcessorFacade(
            resources_provider=RESOURCES_PROVIDER)
        _LOG.debug('aws-syndicate has been initialized')
    except ClientError as e:
        raise InternalError(
            f'An unexpected error has occurred trying to init connection: {e}'
        )


def initialize_project_state(do_not_sync_state=False):
    from syndicate.core.project_state.sync_processor import sync_project_state
    global PROJECT_STATE
    if not PROJECT_STATE:
        if not ProjectState.check_if_project_state_exists(CONF_PATH):
            USER_LOG.warning(
                "Config is set and generated, but project state file does not "
                "exist."
            )
            USER_LOG.warning(
                "Generating project state file '.syndicate' from the existing "
                "structure..."
            )
            PROJECT_STATE = ProjectState.build_from_structure(CONFIG)
        else:
            PROJECT_STATE = ProjectState(project_path=CONFIG.project_path)

    if do_not_sync_state:
        return

    sync_project_state()


def validate_temp_credentials_expiration(expiration):
    if not isinstance(expiration, datetime):
        expiration = datetime.fromisoformat(expiration)
    expiration_datetime = expiration - timedelta(
        minutes=CREDENTIALS_EXPIRATION_THRESHOLD_MIN)
    now_datetime = datetime.now(timezone.utc)

    return expiration_datetime <= now_datetime


def prompt_mfa_code():
    mfa_code = input('Please enter your MFA code to generate '
                     'temp credentials: ')
    while 1:
        if len(mfa_code) == 6 and re.match('[0-9]{6}', mfa_code):
            break
        mfa_code = input(f'Token code must consist of 6 numbers. '
                         f'Try again: ')
    return mfa_code


def initialize_signal_handling():
    from signal import SIGINT, signal
    signal(SIGINT, handle_interruption)
