#!/usr/bin/env python3
#
#  Copyright 2017 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 sys
import re
import yaml
import base
import os
# Load up all wheel and wheel_participant routes
import wheel
import wheel_participant

import time

class Ref(str): pass
class GetAtt(str): pass
yaml.add_representer(Ref, lambda dumper, data: dumper.represent_scalar(u'!Ref', str(data)))
yaml.add_representer(GetAtt, lambda dumper, data: dumper.represent_scalar(u'!GetAtt', str(data)))


S3_PROXY_HEADERS = ['content-type', 'Content-Type', 'Date', 'content-length', 'Content-Length', 'Etag', 'etag']

# Recursive finder for config references
def find_refs(config):
    if isinstance(config, dict):
        return find_refs(list(config.values()))
    elif isinstance(config, list):
        refs = set()
        for item in config:
            refs.update(find_refs(item))
        return refs
    elif isinstance(config, Ref):
        return {str(config)}
    else:
        return set()


PATH_PARAMETER_MATCHER = re.compile(r'{([a-zA-Z0-9_]+)\+?}')


def path_to_parameters(path):
    return PATH_PARAMETER_MATCHER.findall(path)


def snake_case_to_capitalized_words(string):
    return ''.join([s.capitalize() for s in string.split('_')])


def make_api_path_config(lambda_name, path):
    path_config = {
        'x-amazon-apigateway-integration': {
            'contentHandling': 'CONVERT_TO_TEXT',
            'httpMethod': 'POST',
            'passthroughBehavior': 'WHEN_NO_MATCH',
            'responses': {'default': {'statusCode': 200}},
            'type': 'aws_proxy',
            'uri': {
                'Fn::Join': ['', [
                    'arn:aws:apigateway:',
                    Ref('AWS::Region'),
                    ':lambda:path/2015-03-31/functions/arn:aws:lambda:',
                    Ref('AWS::Region'),
                    ':',
                    Ref('AWS::AccountId'),
                    ':function:',
                    Ref(lambda_name),
                    '/invocations'
                ]]
            }
        },
    }

    if path != '/config':  # The configuration variables need to be retrieved without security
        path_config['security'] = [{'apiUsers': []}]

    parameters = path_to_parameters(path)
    if parameters:
        path_config['parameters'] = [{'in': 'path', 'name': p, 'required': True, 'type': 'string'} for p in parameters]
    return path_config


class TemplateCompiler:
    def __init__(self, in_dir, out_dir, *filenames):
        self.in_dir = in_dir
        self.out_dir = out_dir
        self.filenames = filenames

    def __enter__(self):
        self.configs = [yaml.safe_load(open(os.path.join(self.in_dir, name))) for name in self.filenames]
        for config in self.configs:
            config.setdefault('Resources', {})
            config.setdefault('Outputs', {})
        return tuple(self.configs)

    def __exit__(self, exc_type, exc_val, exc_tb):
        if not exc_type:
            global_resources = {}

            for template_filename, config in zip(self.filenames, self.configs):
                stack_prefix = snake_case_to_capitalized_words(template_filename.split('.')[0])
                stack_name = f'{stack_prefix}Stack'

                for ref in find_refs(config):
                    if ref not in config['Resources'] and not ref.startswith('AWS::'):
                        config.setdefault('Parameters', {})
                        config['Parameters'][ref] = {'Type': 'String'}

                for resource in list(config['Resources'].keys()):
                    if config['Resources'][resource]['Type'] == 'AWS::ApiGateway::Deployment':
                        # Unique-ify the logical ID to force a new deployment for each stack update
                        unique_resource_name = f"{resource}{str(int(time.time()))}"
                        config['Resources'][unique_resource_name] = config['Resources'].pop(resource)
                        config['Outputs'][resource] = {'Value': Ref(unique_resource_name)}
                    else:
                        config['Outputs'][resource] = {'Value': Ref(resource)}

                for output in config['Outputs']:
                    global_resources[output] = stack_name

                with open(os.path.join(self.out_dir, template_filename), 'w') as f:
                    f.write(yaml.dump(config, default_flow_style=False))

            # Compile the overall configuration
            overall_config = yaml.safe_load(open(os.path.join(self.in_dir, 'aws-ops-wheel.yml')))
            overall_config.setdefault('Resources', {})
            overall_config.setdefault('Outputs', {})
            for template_filename, config in zip(self.filenames, self.configs):
                stack_prefix = snake_case_to_capitalized_words(template_filename.split('.')[0])
                stack_name = f'{stack_prefix}Stack'
                params = {}
                for p in config.get('Parameters', dict()):
                    if p in global_resources:
                        params[p] = GetAtt(f"{global_resources[p]}.Outputs.{p}")
                    else:
                        overall_config.setdefault('Parameters', dict())
                        overall_config['Parameters'][p] = config['Parameters'][p]
                        params[p] = Ref(p)
                overall_config['Resources'][stack_name] = {
                    'Type': "AWS::CloudFormation::Stack",
                    'Properties': {
                        'TemplateURL': f'./compiled_templates/{template_filename}',
                        'TimeoutInMinutes': 20,
                        'Parameters': params,
                    }
                }
                for p in global_resources:
                    overall_config['Outputs'][p] = {'Value': GetAtt(f"{global_resources[p]}.Outputs.{p}")}


            with open(os.path.join(self.out_dir, 'aws-ops-wheel.yml'), 'w') as f:
                f.write(yaml.dump(overall_config))

def main():
    in_dir, out_dir, static_asset_s3_prefix = sys.argv[1:4]
    static_asset_s3_prefix = static_asset_s3_prefix.strip('/')

    # Unfortunately we've had to split our template into multiple configs with the API config at the top so that
    # we could get past the 50kb limit of CloudFormation
    with TemplateCompiler(
            in_dir, out_dir,
            'cognito.yml', 'lambda.yml', 'api_gateway.yml', 'api_gateway_lambda_roles.yml') as configs:
        cognito_config, lambda_config, api_config, api_lambda_roles_config = configs
        paths = {}
        for func in base.route.registry.values():
            lambda_name = snake_case_to_capitalized_words(func.__name__) + 'Lambda'
            # Strip the parameter and return documentation out of the Lambda description as this confuses Lambda
            lambda_description = ''
            if func.__doc__:
                for line in func.__doc__.splitlines():
                    line = line.strip()
                    if (':param' or ':return') in line:
                        break
                    if line:
                        lambda_description += f'{line} '
            # Generate Lambda Resources
            lambda_config['Resources'][lambda_name] = {
                'Type': 'AWS::Lambda::Function',
                'Properties': {
                    'Code': './build',
                    'Description': lambda_description,
                    'Environment': {
                        'Variables': {
                            'APP_CLIENT_ID': Ref('CognitoUserPoolClient'),
                            'USER_POOL_ID': Ref('CognitoUserPool'),
                            'PARTICIPANT_TABLE': Ref('participantDynamoDBTable'),
                            'WHEEL_TABLE': Ref('wheelDynamoDBTable'),
                        }
                    },
                    'Handler': f"{func.__module__}.{func.__name__}",
                    'MemorySize': 128,
                    'Role': GetAtt('AWSOpsWheelLambdaRole.Arn'),
                    'Runtime': 'python3.9',
                    'Timeout': 3
                }
            }

            path = f'/api/{func.route.path.lstrip("/")}'
            paths.setdefault(path, {})
            for method in func.route.methods:
                paths[path][method.lower()] = make_api_path_config(lambda_name, func.route.path)
                stripped_path = path.lstrip('/')
                api_lambda_roles_config['Resources'][f"{lambda_name}GatewayPermissions{method}"] = {
                    'Type': 'AWS::Lambda::Permission',
                    'Properties': {
                        'Action': 'lambda:invokeFunction',
                        'FunctionName': Ref(lambda_name),
                        'Principal': 'apigateway.amazonaws.com',
                        'SourceArn': {'Fn::Join': ['', [
                            'arn:aws:execute-api:',
                            Ref('AWS::Region'),
                            ':',
                            Ref('AWS::AccountId'),
                            ':',
                            Ref("AWSOpsWheelAPI"),
                            f"/*/{method.upper()}/{stripped_path}",
                        ]]},
                    }
                }

        paths['/favicon.ico'] = {'get': {
            'produces': [ 'image/x-icon' ],
            'responses': {
                '200': {
                    'description': '200 response',
                    'schema': {
                        '$ref': '#/definitions/Empty'
                    },
                    'headers': {
                        'Content-Length': {
                            'type': 'string'
                        },
                        'Content-Type': {
                            'type': 'string'
                        }
                    }
                }
            },
            'x-amazon-apigateway-integration': {
              'responses': {
                'default': {
                  'statusCode': '200',
                  'responseParameters': {
                    'method.response.header.Content-Type': 'integration.response.header.Content-Type',
                    'method.response.header.Content-Length': 'integration.response.header.Content-Length'
                  },
                  'contentHandling': 'CONVERT_TO_BINARY'
                }
              },
              'uri': f'{static_asset_s3_prefix}/favicon.ico',
              'passthroughBehavior': 'when_no_match',
              'httpMethod': 'GET',
              'contentHandling': 'CONVERT_TO_BINARY',
              'type': 'http'
            }
        }}

        paths['/static/{proxy+}'] = {'x-amazon-apigateway-any-method': {
            'parameters': [{'in': 'path', 'name': 'proxy', 'required': True, 'type': 'string'}],
            'produces': ['application/json'],
            'responses': {},
            'x-amazon-apigateway-integration': {
                'cacheKeyParameters': ['method.request.path.proxy'],
                'cacheNamespace': 'static_assets',
                'httpMethod': 'ANY',
                'passthroughBehavior': 'when_no_match',
                'requestParameters': {'integration.request.path.proxy': 'method.request.path.proxy'},
                'responses': {'default': {'statusCode': '200'}},
                'type': 'http_proxy',
                'uri': f'{static_asset_s3_prefix}/{{proxy}}',
                'contentHandling': 'CONVERT_TO_BINARY'}
        }}

        paths['/'] = {'x-amazon-apigateway-any-method': {
            'parameters': [],
            'produces': ['application/json'],
            'responses': {},
            'x-amazon-apigateway-integration': {
                'httpMethod': 'ANY',
                'passthroughBehavior': 'when_no_match',
                'requestParameters': {},
                'responses': {'default': {'statusCode': '200'}},
                'type': 'http_proxy',
                'uri': f'{static_asset_s3_prefix}/index.production.html'}
        }}

        paths['/{proxy+}'] = {'x-amazon-apigateway-any-method': {
            'parameters': [{'in': 'path', 'name': 'proxy', 'required': False, 'type': 'string'}],
            'produces': ['application/json'],
            'responses': {},
            'x-amazon-apigateway-integration': {
                'cacheKeyParameters': ['method.request.path.proxy'],
                'cacheNamespace': 'static_assets',
                'httpMethod': 'ANY',
                'passthroughBehavior': 'when_no_match',
                'requestParameters': {'integration.request.path.proxy': 'method.request.path.proxy'},
                'responses': {'default': {'statusCode': '200'}},
                'type': 'http_proxy',
                'uri': f'{static_asset_s3_prefix}/index.production.html'}
        }}

        api_config['Resources']['AWSOpsWheelAPI']['Properties']['Body'] = {
            'schemes': ['https'],
            'swagger': '2.0',
            'info': {'title': 'AWSOpsWheel', 'version': '0.1'},
            'definitions': {
                'Empty': {'title': 'Empty Schema', 'type': 'object'}
            },
            'x-amazon-apigateway-binary-media-types': ['audio/mpeg', 'audio/*', 'image/x-icon', 'application/font*', 'font/*'],
            'basePath': '/',
            'paths': paths,
            'securityDefinitions': {
                'apiUsers': {
                    'type': 'apiKey',
                    'name': 'Authorization',
                    'in': 'header',
                    'x-amazon-apigateway-authtype': 'cognito_user_pools',
                    'x-amazon-apigateway-authorizer': {
                        'type': 'COGNITO_USER_POOLS',
                        'providerARNs': [Ref('CognitoUserPoolArn')]
                    }
                }
            }
        }


if __name__ == '__main__':
    main()
