# Copyright 2017 Rackspace US, 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.

from collections import namedtuple
import sys

import jinja2
from ruamel import yaml
import voluptuous as volup

import yolo.exceptions
from yolo import utils

PY3 = sys.version_info >= (2, 8)
if PY3:
    unicode = str

STRING_SCHEMA = volup.Any(str, unicode)
STRING_OR_DICT_SCHEMA = volup.Any(str, unicode, dict)

AWSAccount = namedtuple('AWSAccount', ['name', 'account_number'])


class YoloFile(object):
    """Object representation of a yolo.yaml file."""
    DEFAULT_STAGE = 'default'

    # 'accounts' section
    ACCOUNT_SCHEMA = volup.Schema({
        volup.Required('name'): STRING_SCHEMA,
        volup.Required('account_number'): STRING_SCHEMA,
    })
    ACCOUNTS_SCHEMA = volup.Schema([ACCOUNT_SCHEMA])
    # 'templates' section
    ACCOUNT_TEMPLATE_SCHEMA = volup.Schema({
        volup.Required('path'): STRING_SCHEMA,
        volup.Optional('params'): {STRING_SCHEMA: STRING_SCHEMA},
        volup.Required('region'): STRING_SCHEMA,
    })
    STAGE_TEMPLATE_SCHEMA = volup.Schema({
        volup.Required('path'): STRING_SCHEMA,
        volup.Optional('params'): {STRING_SCHEMA: STRING_SCHEMA},
    })
    TEMPLATES_SCHEMA = volup.Schema({
        volup.Optional('account'): ACCOUNT_TEMPLATE_SCHEMA,
        volup.Required('stage'): STAGE_TEMPLATE_SCHEMA,
    })
    # 'stages' section
    STAGES_SCHEMA = volup.Schema({
        STRING_SCHEMA: {  # stage name, arbitrary string
            volup.Required('account'): STRING_SCHEMA,
            volup.Required('region'): STRING_SCHEMA,
            volup.Optional('protected'): bool,
            volup.Optional('params'): {STRING_SCHEMA: STRING_SCHEMA},
        },
    })
    # 'services' section
    PARAMETERS_SCHEMA = volup.Schema({
        # Key is the stage name.
        # Config items are defined as a list of dicts.
        volup.Required('stages'): {
            # The key here is the stage name.
            STRING_SCHEMA: [{
                # Parameter name:
                volup.Required('name'): STRING_SCHEMA,
                # Specify a value inline if the value isn't secret/sensitive.
                volup.Optional('value'): STRING_SCHEMA,
                # Indicate if the value should be collected as a multiline string,
                # such as in the case of a certificate or private key block.
                volup.Optional('multiline', default=False): bool,
            }],
        },
    })
    SUPPORTED_RUNTIMES = [
        'python2.7',
        'python3.6',
    ]
    YOKE_LAMBDA_FN_CFG = volup.Schema({
        volup.Required('FunctionName'): STRING_SCHEMA,
        volup.Required('Role'): STRING_SCHEMA,
        volup.Required('Handler'): STRING_SCHEMA,
        volup.Optional('Description'): STRING_SCHEMA,
        volup.Optional('Timeout'): int,
        volup.Optional('MemorySize'): int,
        volup.Optional('VpcConfig'): {
            volup.Required('SubnetIds'): [str],
            volup.Required('SecurityGroupIds'): [str],
        },
        volup.Optional('Environment'): {
            'Variables': {STRING_SCHEMA: STRING_SCHEMA},
        },
        volup.Optional('Runtime'): volup.Any(*SUPPORTED_RUNTIMES),
        volup.Optional('TracingConfig'): {
            volup.Required('Mode'): volup.Any('Active', 'PassThrough'),
        },
    })
    YOKE_SCHEMA = volup.Schema({
        # value is a dictionary of strings or dicts, keyed by strings
        volup.Required('environment'): {
            STRING_SCHEMA: STRING_OR_DICT_SCHEMA,
        },
        volup.Optional('working_dir'): STRING_SCHEMA,
        volup.Optional('stage'): STRING_SCHEMA,
    })
    APIGATEWAY_SCHEMA = volup.Schema({
        volup.Optional('custom_domain'): STRING_OR_DICT_SCHEMA,
        volup.Optional('base_path'): STRING_SCHEMA,
        volup.Optional('rest_api_name'): STRING_SCHEMA,
    })
    SERVICE_TYPE_S3 = 's3'
    SERVICE_TYPE_LAMBDA = 'lambda'
    SERVICE_TYPE_LAMBDA_APIGATEWAY = 'lambda-apigateway'
    SERVICE_TYPES = (
        SERVICE_TYPE_S3,
        SERVICE_TYPE_LAMBDA,
        SERVICE_TYPE_LAMBDA_APIGATEWAY,
    )
    SERVICES_SCHEMA = volup.Schema({
        # Many services, keyed by name
        STRING_SCHEMA: {  # service name, arbitary string
            volup.Required('type'): volup.Any(*SERVICE_TYPES),
            # Only required for S3 types services.
            # TODO(larsbutler): It might make sense to also use this for
            # placing lambda service build artifacts (swagger.json,
            # lambda_function.zip, etc.) so that we consistently handle
            # uploads/pushes of builds, rather than being implicit about
            # everything and placing build artifacts into source directories--
            # which is not ideal.
            volup.Optional('dist_path'): STRING_SCHEMA,
            volup.Optional('parameters'): PARAMETERS_SCHEMA,
            volup.Optional('yoke'): YOKE_SCHEMA,
            # TODO(larsbutler): Make these conditional on the service type (s3)
            volup.Optional('bucket_name'): STRING_SCHEMA,
            # TODO(larsbutler): Make these conditional on the service type
            # (lambda-apigateway)
            # Can be a simple dict, or a list of dicts as well.
            volup.Optional('apigateway'): volup.Any(APIGATEWAY_SCHEMA, [APIGATEWAY_SCHEMA]),
            # Only required for lambda/lambda-apigateway services.
            volup.Optional('lambda_function_configuration'): YOKE_LAMBDA_FN_CFG,
        }
    })
    # top-level schema
    YOLOFILE_SCHEMA = volup.Schema({
        volup.Required('name'): STRING_SCHEMA,
        volup.Required('accounts'): ACCOUNTS_SCHEMA,
        volup.Required('templates'): TEMPLATES_SCHEMA,
        volup.Required('stages'): STAGES_SCHEMA,
        volup.Required('services'): SERVICES_SCHEMA,
    })

    def __init__(self, content):
        """
        :param content:
            `dict` representation of the contents read from a yolo.yaml file.
        """
        self._raw_content = content
        self._validate()
        self.app_name = self._raw_content['name']

    @classmethod
    def from_file(cls, file_obj):
        """Load a yolo.yaml file from an open file-like object."""
        content = yaml.safe_load(file_obj)
        return cls(content)

    @classmethod
    def from_path(cls, path):
        """Load a yolo.yaml file given a path to the file."""
        with open(path) as fp:
            return cls.from_file(fp)

    def to_fileobj(self):
        """Dump this `YoloFile` contents to file-like object."""
        fp = utils.StringIO()
        yaml.dump(self._raw_content, fp, encoding='utf-8',
                  Dumper=yaml.RoundTripDumper)
        fp.seek(0)
        return fp

    def _validate(self):
        self.YOLOFILE_SCHEMA(self._raw_content)

    @property
    def accounts(self):
        return self._raw_content['accounts']

    @property
    def stages(self):
        return self._raw_content['stages']

    @property
    def templates(self):
        return self._raw_content['templates']

    @property
    def services(self):
        return self._raw_content['services']

    def get_stage_config(self, stage):
        if stage in self.stages:
            return self.stages[stage]
        else:
            # use the default/base stage config
            try:
                return self.stages[self.DEFAULT_STAGE]
            except KeyError:
                raise yolo.exceptions.YoloError(
                    'Unable to build custom stage config. Reason: No "default"'
                    'stage is defined.'
                )

    def normalize_account(self, account):
        """Take an account name or number and return an `AWSAccount` instance.

        This is meant to make commands more flexible so that the user can
        specify either the exact account number or the alias defined in the
        `accounts` section of the yolo.yml file.

        :param account:
            The account name or actual account number.
        :returns:
            :class:`AWSAccount` instance.
        :raises:
            :class:`yolo.exceptions.YoloError` if the account name or number
            can't be found.
        """
        # Check if it's an alias or a real number.
        account_name = None
        account_number = None
        for acct in self.accounts:
            if acct['name'] == account:
                # We got an account alias
                account_name = account
                account_number = acct['account_number']
                break
            elif acct['account_number'] == account:
                # We found a matching account
                account_name = acct['name']
                account_number = account
                break
        else:
            # We didn't find a matching account number or alias
            raise yolo.exceptions.YoloError(
                'Unable to find a matching account number or alias for '
                '"{}"'.format(account)
            )
        return AWSAccount(name=account_name, account_number=account_number)

    def render(self, **variables):
        # Render variables into the yolo file.
        template = jinja2.Template(
            yaml.dump(self._raw_content, Dumper=yaml.RoundTripDumper)
        )
        rendered_content = template.render(**variables)
        new_content = yaml.safe_load(rendered_content)
        return YoloFile(new_content)

    def _is_baseline_infrastructure_defined(self):
        return 'account' in self.templates
