# Copyright 2014
# The Cloudscaling Group, 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 random
import string

from lxml import etree
import netaddr
from neutronclient.common import exceptions as neutron_exception
from oslo_log import log as logging
import six

from ec2api.api import common
from ec2api.api import ec2utils
from ec2api import clients
from ec2api.db import api as db_api
from ec2api import exception
from ec2api.i18n import _


LOG = logging.getLogger(__name__)


"""VPN connections related API implementation
"""


Validator = common.Validator


SHARED_KEY_CHARS = string.ascii_letters + '_.' + string.digits
AWS_MSS = 1387
MTU_MSS_DELTA = 40  # 20 byte IP and 20 byte TCP headers


def create_vpn_connection(context, customer_gateway_id, vpn_gateway_id,
                          type, options=None):
    if not options or options.get('static_routes_only') is not True:
        raise exception.Unsupported('BGP dynamic routing is unsupported')
    customer_gateway = ec2utils.get_db_item(context, customer_gateway_id)
    vpn_gateway = ec2utils.get_db_item(context, vpn_gateway_id)
    vpn_connection = next(
        (vpn for vpn in db_api.get_items(context, 'vpn')
         if vpn['customer_gateway_id'] == customer_gateway_id),
        None)
    if vpn_connection:
        if vpn_connection['vpn_gateway_id'] == vpn_gateway_id:
            ec2_vpn_connections = describe_vpn_connections(
                context, vpn_connection_id=[vpn_connection['id']])
            return {
                'vpnConnection': ec2_vpn_connections['vpnConnectionSet'][0]}
        else:
            raise exception.InvalidCustomerGatewayDuplicateIpAddress()
    neutron = clients.neutron(context)
    with common.OnCrashCleaner() as cleaner:
        os_ikepolicy = {'ike_version': 'v1',
                        'auth_algorithm': 'sha1',
                        'encryption_algorithm': 'aes-128',
                        'pfs': 'group2',
                        'phase1_negotiation_mode': 'main',
                        'lifetime': {'units': 'seconds',
                                     'value': 28800}}
        os_ikepolicy = neutron.create_ikepolicy(
            {'ikepolicy': os_ikepolicy})['ikepolicy']
        cleaner.addCleanup(neutron.delete_ikepolicy, os_ikepolicy['id'])

        os_ipsecpolicy = {'transform_protocol': 'esp',
                          'auth_algorithm': 'sha1',
                          'encryption_algorithm': 'aes-128',
                          'pfs': 'group2',
                          'encapsulation_mode': 'tunnel',
                          'lifetime': {'units': 'seconds',
                                       'value': 3600}}
        os_ipsecpolicy = neutron.create_ipsecpolicy(
            {'ipsecpolicy': os_ipsecpolicy})['ipsecpolicy']
        cleaner.addCleanup(neutron.delete_ipsecpolicy, os_ipsecpolicy['id'])

        psk = ''.join(random.choice(SHARED_KEY_CHARS) for _x in range(32))
        vpn_connection = db_api.add_item(
             context, 'vpn',
             {'customer_gateway_id': customer_gateway['id'],
              'vpn_gateway_id': vpn_gateway['id'],
              'pre_shared_key': psk,
              'os_ikepolicy_id': os_ikepolicy['id'],
              'os_ipsecpolicy_id': os_ipsecpolicy['id'],
              'cidrs': [],
              'os_ipsec_site_connections': {}})
        cleaner.addCleanup(db_api.delete_item, context, vpn_connection['id'])

        neutron.update_ikepolicy(
            os_ikepolicy['id'], {'ikepolicy': {'name': vpn_connection['id']}})
        neutron.update_ipsecpolicy(
            os_ipsecpolicy['id'],
            {'ipsecpolicy': {'name': vpn_connection['id']}})

        _reset_vpn_connections(context, neutron, cleaner,
                               vpn_gateway, vpn_connections=[vpn_connection])

    ec2_vpn_connections = describe_vpn_connections(
        context, vpn_connection_id=[vpn_connection['id']])
    return {
        'vpnConnection': ec2_vpn_connections['vpnConnectionSet'][0]}


def create_vpn_connection_route(context, vpn_connection_id,
                                destination_cidr_block):
    vpn_connection = ec2utils.get_db_item(context, vpn_connection_id)
    if destination_cidr_block in vpn_connection['cidrs']:
        return True
    neutron = clients.neutron(context)
    vpn_gateway = db_api.get_item_by_id(context,
                                        vpn_connection['vpn_gateway_id'])
    with common.OnCrashCleaner() as cleaner:
        _add_cidr_to_vpn_connection_item(context, vpn_connection,
                                         destination_cidr_block)
        cleaner.addCleanup(_remove_cidr_from_vpn_connection_item,
                           context, vpn_connection, destination_cidr_block)

        _reset_vpn_connections(context, neutron, cleaner,
                               vpn_gateway, vpn_connections=[vpn_connection])

    return True


def delete_vpn_connection_route(context, vpn_connection_id,
                                destination_cidr_block):
    vpn_connection = ec2utils.get_db_item(context, vpn_connection_id)
    if destination_cidr_block not in vpn_connection['cidrs']:
        raise exception.InvalidRouteNotFound(
            _('The specified route %(destination_cidr_block)s does not exist')
            % {'destination_cidr_block': destination_cidr_block})
    neutron = clients.neutron(context)
    vpn_gateway = db_api.get_item_by_id(context,
                                        vpn_connection['vpn_gateway_id'])
    with common.OnCrashCleaner() as cleaner:
        _remove_cidr_from_vpn_connection_item(context, vpn_connection,
                                              destination_cidr_block)
        cleaner.addCleanup(_add_cidr_to_vpn_connection_item,
                           context, vpn_connection, destination_cidr_block)

        _reset_vpn_connections(context, neutron, cleaner,
                               vpn_gateway, vpn_connections=[vpn_connection])

    return True


def delete_vpn_connection(context, vpn_connection_id):
    vpn_connection = ec2utils.get_db_item(context, vpn_connection_id)
    with common.OnCrashCleaner() as cleaner:
        db_api.delete_item(context, vpn_connection['id'])
        cleaner.addCleanup(db_api.restore_item, context, 'vpn', vpn_connection)
        neutron = clients.neutron(context)
        _stop_vpn_connection(neutron, vpn_connection)
        try:
            neutron.delete_ipsecpolicy(vpn_connection['os_ipsecpolicy_id'])
        except neutron_exception.Conflict as ex:
            LOG.warning('Failed to delete ipsecoplicy %(os_id)s during '
                        'deleting VPN connection %(id)s. Reason: %(reason)s',
                        {'id': vpn_connection['id'],
                         'os_id': vpn_connection['os_ipsecpolicy_id'],
                         'reason': ex.message})
        except neutron_exception.NotFound:
            pass
        try:
            neutron.delete_ikepolicy(vpn_connection['os_ikepolicy_id'])
        except neutron_exception.Conflict as ex:
            LOG.warning(
                'Failed to delete ikepolicy %(os_id)s during deleting '
                'VPN connection %(id)s. Reason: %(reason)s',
                {'id': vpn_connection['id'],
                 'os_id': vpn_connection['os_ikepolicy_id'],
                 'reason': ex.message})
        except neutron_exception.NotFound:
            pass
    return True


def describe_vpn_connections(context, vpn_connection_id=None, filter=None):
    formatted_vpn_connections = VpnConnectionDescriber().describe(
        context, ids=vpn_connection_id, filter=filter)
    return {'vpnConnectionSet': formatted_vpn_connections}


class VpnConnectionDescriber(common.TaggableItemsDescriber,
                             common.NonOpenstackItemsDescriber):

    KIND = 'vpn'
    FILTER_MAP = {'customer-gateway-configuration': (
                        'customerGatewayConfiguration'),
                  'customer-gateway-id': 'customerGatewayId',
                  'state': 'state',
                  'option.static-routes-only': ('options', 'staticRoutesOnly'),
                  'route.destination-cidr-block': ['routes',
                                                   'destinationCidrBlock'],
                  'type': 'type',
                  'vpn-connection-id': 'vpnConnectionId',
                  'vpn-gateway-id': 'vpnGatewayId'}

    def get_db_items(self):
        self.customer_gateways = {
            cgw['id']: cgw
            for cgw in db_api.get_items(self.context, 'cgw')}
        neutron = clients.neutron(self.context)
        self.os_ikepolicies = {
            ike['id']: ike
            for ike in neutron.list_ikepolicies(
                tenant_id=self.context.project_id)['ikepolicies']}
        self.os_ipsecpolicies = {
            ipsec['id']: ipsec
            for ipsec in neutron.list_ipsecpolicies(
                tenant_id=self.context.project_id)['ipsecpolicies']}
        self.os_ipsec_site_connections = {
            conn['id']: conn
            for conn in neutron.list_ipsec_site_connections(
                tenant_id=self.context.project_id)['ipsec_site_connections']}
        self.external_ips = _get_vpn_gateways_external_ips(
            self.context, neutron)
        return super(VpnConnectionDescriber, self).get_db_items()

    def format(self, vpn_connection):
        return _format_vpn_connection(
            vpn_connection, self.customer_gateways, self.os_ikepolicies,
            self.os_ipsecpolicies, self.os_ipsec_site_connections,
            self.external_ips)


def _format_vpn_connection(vpn_connection, customer_gateways, os_ikepolicies,
                           os_ipsecpolicies, os_ipsec_site_connections,
                           external_ips):
    config_dict = _format_customer_config(
        vpn_connection, customer_gateways, os_ikepolicies, os_ipsecpolicies,
        os_ipsec_site_connections, external_ips)
    config = ec2utils.dict_to_xml(config_dict, 'vpn_connection')
    config.attrib['id'] = vpn_connection['id']
    config_str = etree.tostring(config, xml_declaration=True, encoding='UTF-8',
                                pretty_print=True)
    return {'vpnConnectionId': vpn_connection['id'],
            'vpnGatewayId': vpn_connection['vpn_gateway_id'],
            'customerGatewayId': vpn_connection['customer_gateway_id'],
            'state': 'available',
            'type': 'ipsec.1',
            'routes': [{'destinationCidrBlock': cidr,
                        'state': 'available'}
                       for cidr in vpn_connection['cidrs']],
            'vgwTelemetry': [],
            'options': {'staticRoutesOnly': True},
            'customerGatewayConfiguration': config_str}


def _format_customer_config(vpn_connection, customer_gateways, os_ikepolicies,
                            os_ipsecpolicies, os_ipsec_site_connections,
                            external_ips):
    customer_gateway = customer_gateways[vpn_connection['customer_gateway_id']]
    os_connections_ids = vpn_connection['os_ipsec_site_connections'].values()
    if os_connections_ids:
        os_ipsec_site_connection = next(
            (os_ipsec_site_connections[conn_id]
             for conn_id in os_connections_ids
             if os_ipsec_site_connections.get(conn_id)),
            None)
    else:
        os_ipsec_site_connection = None

    # TODO(ft): figure out and add to the output tunnel internal addresses
    config_dict = {
        'customer_gateway_id': vpn_connection['customer_gateway_id'],
        'vpn_gateway_id': vpn_connection['vpn_gateway_id'],
        'vpn_connection_type': 'ipsec.1',
        'vpn_connection_attributes': 'NoBGPVPNConnection',
        'ipsec_tunnel': {
            'customer_gateway': {
                'tunnel_outside_address': {
                    'ip_address': (
                        os_ipsec_site_connection['peer_address']
                        if os_ipsec_site_connection else
                        customer_gateway['ip_address'])}},
            'vpn_gateway': {
                'tunnel_outside_address': {
                    'ip_address': external_ips.get(
                        vpn_connection['vpn_gateway_id'])}}},
    }
    os_ikepolicy = os_ikepolicies.get(vpn_connection['os_ikepolicy_id'])
    if os_ikepolicy:
        config_dict['ipsec_tunnel']['ike'] = {
            'authentication_protocol': os_ikepolicy['auth_algorithm'],
            'encryption_protocol': os_ikepolicy['encryption_algorithm'],
            'lifetime': os_ikepolicy['lifetime']['value'],
            'perfect_forward_secrecy': os_ikepolicy['pfs'],
            'mode': os_ikepolicy['phase1_negotiation_mode'],
            'pre_shared_key': (
                os_ipsec_site_connection['psk']
                if os_ipsec_site_connection else
                vpn_connection['pre_shared_key']),
        }
    os_ipsecpolicy = os_ipsecpolicies.get(vpn_connection['os_ipsecpolicy_id'])
    if os_ipsecpolicy:
        config_dict['ipsec_tunnel']['ipsec'] = {
            'protocol': os_ipsecpolicy['transform_protocol'],
            'authentication_protocol': os_ipsecpolicy['auth_algorithm'],
            'encryption_protocol': os_ipsecpolicy['encryption_algorithm'],
            'lifetime': os_ipsecpolicy['lifetime']['value'],
            'perfect_forward_secrecy': os_ipsecpolicy['pfs'],
            'mode': os_ipsecpolicy['encapsulation_mode'],
            'tcp_mss_adjustment': (
                os_ipsec_site_connection['mtu'] - MTU_MSS_DELTA
                if os_ipsec_site_connection else
                AWS_MSS),
        }
    return config_dict


def _stop_vpn_connection(neutron, vpn_connection):
    connection_ids = vpn_connection['os_ipsec_site_connections']
    for os_connection_id in six.itervalues(connection_ids):
        try:
            neutron.delete_ipsec_site_connection(os_connection_id)
        except neutron_exception.NotFound:
            pass


def _stop_gateway_vpn_connections(context, neutron, cleaner, vpn_gateway):
    def undo_vpn_connection(context, vpn_connection, connections_ids):
        vpn_connection['os_ipsec_site_connections'] = connections_ids
        db_api.update_item(context, vpn_connection)

    for vpn_connection in db_api.get_items(context, 'vpn'):
        if vpn_connection['vpn_gateway_id'] == vpn_gateway['id']:
            _stop_vpn_connection(neutron, vpn_connection)

            connection_ids = vpn_connection['os_ipsec_site_connections']
            vpn_connection['os_ipsec_site_connections'] = {}
            db_api.update_item(context, vpn_connection)
            cleaner.addCleanup(undo_vpn_connection, context, vpn_connection,
                               connection_ids)


def _update_vpn_routes(context, neutron, cleaner, route_table, subnets):
    vpn_gateway = ec2utils.get_attached_gateway(
        context, route_table['vpc_id'], 'vgw')
    if not vpn_gateway:
        return
    _reset_vpn_connections(context, neutron, cleaner, vpn_gateway,
                           route_tables=[route_table], subnets=subnets)


def _reset_vpn_connections(context, neutron, cleaner, vpn_gateway,
                           subnets=None, route_tables=None,
                           vpn_connections=None):
    if not vpn_gateway['vpc_id']:
        return
    # TODO(ft): implement search filters in DB api
    vpn_connections = (vpn_connections or
                       [vpn for vpn in db_api.get_items(context, 'vpn')
                        if vpn['vpn_gateway_id'] == vpn_gateway['id']])
    if not vpn_connections:
        return
    subnets = (subnets or
               [subnet for subnet in db_api.get_items(context, 'subnet')
                if subnet['vpc_id'] == vpn_gateway['vpc_id']])
    if not subnets:
        return
    vpc = db_api.get_item_by_id(context, vpn_gateway['vpc_id'])
    customer_gateways = {cgw['id']: cgw
                         for cgw in db_api.get_items(context, 'cgw')}
    route_tables = route_tables or db_api.get_items(context, 'rtb')
    route_tables = {rtb['id']: rtb
                    for rtb in route_tables
                    if rtb['vpc_id'] == vpc['id']}
    route_tables_cidrs = {}
    for subnet in subnets:
        route_table_id = subnet.get('route_table_id', vpc['route_table_id'])
        if route_table_id not in route_tables_cidrs:
            route_tables_cidrs[route_table_id] = (
                _get_route_table_vpn_cidrs(route_tables[route_table_id],
                                           vpn_gateway, vpn_connections))
        cidrs = route_tables_cidrs[route_table_id]
        for vpn_conn in vpn_connections:
            if vpn_conn['id'] in cidrs:
                _set_subnet_vpn(
                    context, neutron, cleaner, subnet, vpn_conn,
                    customer_gateways[vpn_conn['customer_gateway_id']],
                    cidrs[vpn_conn['id']])
            else:
                _delete_subnet_vpn(context, neutron, cleaner, subnet, vpn_conn)


def _set_subnet_vpn(context, neutron, cleaner, subnet, vpn_connection,
                    customer_gateway, cidrs):
    subnets_connections = vpn_connection['os_ipsec_site_connections']
    os_connection_id = subnets_connections.get(subnet['id'])
    if os_connection_id:
        # TODO(ft): restore original peer_cidrs on crash
        neutron.update_ipsec_site_connection(
            os_connection_id,
            {'ipsec_site_connection': {'peer_cidrs': cidrs}})
    else:
        os_connection = {
            'vpnservice_id': subnet['os_vpnservice_id'],
            'ikepolicy_id': vpn_connection['os_ikepolicy_id'],
            'ipsecpolicy_id': vpn_connection['os_ipsecpolicy_id'],
            'peer_address': customer_gateway['ip_address'],
            'peer_cidrs': cidrs,
            'psk': vpn_connection['pre_shared_key'],
            'name': '%s/%s' % (vpn_connection['id'], subnet['id']),
            'peer_id': customer_gateway['ip_address'],
            'mtu': AWS_MSS + MTU_MSS_DELTA,
            'initiator': 'response-only',
        }
        os_connection = (neutron.create_ipsec_site_connection(
            {'ipsec_site_connection': os_connection})
            ['ipsec_site_connection'])
        cleaner.addCleanup(neutron.delete_ipsec_site_connection,
                           os_connection['id'])

        _add_subnet_connection_to_vpn_connection_item(
            context, vpn_connection, subnet['id'], os_connection['id'])
        cleaner.addCleanup(_remove_subnet_connection_from_vpn_connection_item,
                           context, vpn_connection, subnet['id'])


def _delete_subnet_vpn(context, neutron, cleaner, subnet, vpn_connection):
    subnets_connections = vpn_connection['os_ipsec_site_connections']
    os_connection_id = subnets_connections.get(subnet['id'])
    if not os_connection_id:
        return

    _remove_subnet_connection_from_vpn_connection_item(
        context, vpn_connection, subnet['id'])
    cleaner.addCleanup(_add_subnet_connection_to_vpn_connection_item,
                       context, vpn_connection, subnet['id'], os_connection_id)
    try:
        neutron.delete_ipsec_site_connection(os_connection_id)
    except neutron_exception.NotFound:
        pass


def _get_route_table_vpn_cidrs(route_table, vpn_gateway, vpn_connections):
    static_cidrs = [route['destination_cidr_block']
                    for route in route_table['routes']
                    if route.get('gateway_id') == vpn_gateway['id']]
    is_propagation_enabled = (
        vpn_gateway['id'] in route_table.get('propagating_gateways', []))
    vpn_cidrs = {}
    for vpn in vpn_connections:
        if is_propagation_enabled:
            cidrs = list(set(static_cidrs + vpn['cidrs']))
        else:
            cidrs = static_cidrs
        if cidrs:
            vpn_cidrs[vpn['id']] = cidrs
    return vpn_cidrs


def _get_vpn_gateways_external_ips(context, neutron):
    vpcs = {vpc['id']: vpc
            for vpc in db_api.get_items(context, 'vpc')}
    external_ips = {}
    routers = neutron.list_routers(
        tenant_id=context.project_id)['routers']
    for router in routers:
        info = router['external_gateway_info']
        if info:
            for ip in info['external_fixed_ips']:
                if netaddr.valid_ipv4(ip['ip_address']):
                    external_ips[router['id']] = ip['ip_address']
    return {vgw['id']: external_ips.get(vpcs[vgw['vpc_id']]['os_id'])
            for vgw in db_api.get_items(context, 'vgw')
            if vgw['vpc_id']}


def _add_cidr_to_vpn_connection_item(context, vpn_connection, cidr):
    vpn_connection['cidrs'].append(cidr)
    db_api.update_item(context, vpn_connection)


def _remove_cidr_from_vpn_connection_item(context, vpn_connection, cidr):
    vpn_connection['cidrs'].remove(cidr)
    db_api.update_item(context, vpn_connection)


def _add_subnet_connection_to_vpn_connection_item(context, vpn_connection,
                                                  subnet_id, os_connection_id):
    vpn_connection['os_ipsec_site_connections'][subnet_id] = os_connection_id
    db_api.update_item(context, vpn_connection)


def _remove_subnet_connection_from_vpn_connection_item(context, vpn_connection,
                                                       subnet_id):
    del vpn_connection['os_ipsec_site_connections'][subnet_id]
    db_api.update_item(context, vpn_connection)
