# 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 copy

try:
    from neutronclient.common import exceptions as neutron_exception
except ImportError:
    pass  # clients will log absense of neutronclient in this case
from oslo_config import cfg
from oslo_log import log as logging

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


CONF = cfg.CONF
LOG = logging.getLogger(__name__)


"""Security Groups related API implementation
"""

Validator = common.Validator


SECURITY_GROUP_MAP = {'domain-name-servers': 'dns-servers',
                      'domain-name': 'domain-name',
                      'ntp-servers': 'ntp-server',
                      'netbios-name-servers': 'netbios-ns',
                      'netbios-node-type': 'netbios-nodetype'}

DEFAULT_GROUP_NAME = 'default'


def get_security_group_engine():
    return SecurityGroupEngineNeutron()


def create_security_group(context, group_name, group_description,
                          vpc_id=None):
    if group_name == DEFAULT_GROUP_NAME:
        if vpc_id:
            raise exception.InvalidParameterValue(
                _('Cannot use reserved security group name: %s')
                % DEFAULT_GROUP_NAME)
        else:
            raise exception.InvalidGroupReserved(group_name=group_name)
    filter = [{'name': 'group-name',
               'value': [group_name]}]
    if not vpc_id and CONF.disable_ec2_classic:
        vpc_id = ec2utils.get_default_vpc(context)['id']
    if vpc_id and group_name != vpc_id:
        filter.append({'name': 'vpc-id',
                       'value': [vpc_id]})
    security_groups = describe_security_groups(
        context, filter=filter)['securityGroupInfo']
    if not vpc_id:
        # TODO(andrey-mp): remove it when fitering by None will be implemented
        security_groups = [sg for sg in security_groups
                           if sg.get('vpcId') is None]
    if security_groups:
        raise exception.InvalidGroupDuplicate(name=group_name)
    return _create_security_group(context, group_name, group_description,
                                  vpc_id)


def _create_security_group(context, group_name, group_description,
                           vpc_id=None, default=False):
    neutron = clients.neutron(context)
    with common.OnCrashCleaner() as cleaner:
        try:
            secgroup_body = (
                {'security_group': {'name': group_name,
                                    'description': group_description}})
            os_security_group = neutron.create_security_group(
                secgroup_body)['security_group']
        except neutron_exception.OverQuotaClient:
            raise exception.ResourceLimitExceeded(resource='security groups')
        cleaner.addCleanup(neutron.delete_security_group,
                           os_security_group['id'])
        if vpc_id:
            # NOTE(Alex) Check if such vpc exists
            ec2utils.get_db_item(context, vpc_id)
        item = {'vpc_id': vpc_id, 'os_id': os_security_group['id']}
        if not default:
            security_group = db_api.add_item(context, 'sg', item)
        else:
            item['id'] = ec2utils.change_ec2_id_kind(vpc_id, 'sg')
            # NOTE(andrey-mp): try to add item with specific id
            # and catch exception if it exists
            security_group = db_api.restore_item(context, 'sg', item)
        return {'return': 'true',
                'groupId': security_group['id']}


def _create_default_security_group(context, vpc):
    # NOTE(Alex): OpenStack doesn't allow creation of another group
    # named 'default' hence vpc-id is used.
    try:
        sg_id = _create_security_group(context, vpc['id'],
                               'Default VPC security group', vpc['id'],
                               default=True)['groupId']
    except (exception.EC2DBDuplicateEntry, exception.InvalidVpcIDNotFound):
        # NOTE(andrey-mp): when this thread tries to recreate default group
        # but another thread tries to delete vpc we should pass vpc not found
        LOG.exception('Failed to create default security group.')
        return None
    return sg_id


def delete_security_group(context, group_name=None, group_id=None,
                          delete_default=False):
    if group_name is None and group_id is None:
        raise exception.MissingParameter(param='group id or name')
    security_group_engine.delete_group(context, group_name, group_id,
                                       delete_default)
    return True


class SecurityGroupDescriber(common.TaggableItemsDescriber):

    KIND = 'sg'
    FILTER_MAP = {'description': 'groupDescription',
                  'group-id': 'groupId',
                  'group-name': 'groupName',
                  'ip-permission.cidr': ['ipPermissions',
                                         ['ipRanges', 'cidrIp']],
                  'ip-permission.from-port': ['ipPermissions', 'fromPort'],
                  'ip-permission.group-id': ['ipPermissions',
                                             ['groups', 'groupId']],
                  'ip-permission.group-name': ['ipPermissions',
                                               ['groups', 'groupName']],
                  'ip-permission.protocol': ['ipPermissions', 'ipProtocol'],
                  'ip-permission.to-port': ['ipPermissions', 'toPort'],
                  'ip-permission.user-id': ['ipPermissions',
                                            ['groups', 'userId']],
                  'owner-id': 'ownerId',
                  'vpc-id': 'vpcId',
    }

    def __init__(self, default_vpc_id):
        super(SecurityGroupDescriber, self).__init__()
        self.all_db_items = None
        self.default_vpc_id = default_vpc_id

    def format(self, item=None, os_item=None):
        return _format_security_group(item, os_item,
                                      self.all_db_items, self.os_items)

    def get_os_items(self):
        if self.all_db_items is None:
            self.all_db_items = db_api.get_items(self.context, 'sg')
        os_groups = security_group_engine.get_os_groups(self.context)
        if self.check_and_repair_default_groups(os_groups, self.all_db_items):
            self.all_db_items = db_api.get_items(self.context, 'sg')
            self.items = self.get_db_items()
            os_groups = security_group_engine.get_os_groups(self.context)
        for os_group in os_groups:
            os_group['name'] = _translate_group_name(self.context,
                                                     os_group,
                                                     self.all_db_items)
        return os_groups

    def check_and_repair_default_groups(self, os_groups, db_groups):
        vpcs = ec2utils.get_db_items(self.context, 'vpc', None)
        os_groups_dict = {g['name']: g['id'] for g in os_groups}
        db_groups_dict = {g['os_id']: g['vpc_id'] for g in db_groups}
        had_to_repair = False
        for vpc in vpcs:
            os_group = os_groups_dict.get(vpc['id'])
            if os_group:
                db_group = db_groups_dict.get(os_group)
                if db_group and db_group == vpc['id']:
                    continue
            result = _create_default_security_group(self.context, vpc)
            if result:
                had_to_repair = True
        return had_to_repair

    def is_selected_item(self, context, os_item_name, item):
        if item and item['id'] in self.ids:
            return True
        if os_item_name in self.names:
            if not CONF.disable_ec2_classic:
                return (not item or not item['vpc_id'])
            else:
                return (self.default_vpc_id and item and
                        item['vpc_id'] == self.default_vpc_id)
        return False


def describe_security_groups(context, group_name=None, group_id=None,
                             filter=None):
    default_vpc_id = None
    default_vpc = ec2utils.check_and_create_default_vpc(context)
    if default_vpc:
        default_vpc_id = default_vpc['id']
    formatted_security_groups = SecurityGroupDescriber(
        default_vpc_id).describe(context, group_id, group_name, filter)
    return {'securityGroupInfo': formatted_security_groups}


# TODO(Alex) cidr/ports/protocol/source_group should be possible
# to pass in root set of parameters, not in ip_permissions as now only
# supported, for authorize and revoke functions.
# The new parameters appeared only in the very recent version of AWS doc.
# API version 2014-06-15 didn't claim support of it.

def authorize_security_group_ingress(context, group_id=None,
                                     group_name=None, ip_permissions=None):
    if group_name and not group_id and CONF.disable_ec2_classic:
        sg = describe_security_groups(
            context,
            group_name=[group_name])['securityGroupInfo'][0]
        group_id = sg['groupId']
        group_name = None
    return _authorize_security_group(context, group_id, group_name,
                                     ip_permissions, 'ingress')


def authorize_security_group_egress(context, group_id, ip_permissions=None):
    security_group = ec2utils.get_db_item(context, group_id)
    if not security_group.get('vpc_id'):
        raise exception.InvalidParameterValue(message=_('Only Amazon VPC '
            'security groups may be used with this operation.'))
    return _authorize_security_group(context, group_id, None,
                                     ip_permissions, 'egress')


def _authorize_security_group(context, group_id, group_name,
                              ip_permissions, direction):
    rules_bodies = _build_rules(context, group_id, group_name,
                                ip_permissions, direction)
    for rule_body in rules_bodies:
        security_group_engine.authorize_security_group(context, rule_body)
    return True


def _validate_parameters(protocol, from_port, to_port):
    if (not isinstance(protocol, int) and
            protocol not in ['tcp', 'udp', 'icmp']):
        raise exception.InvalidParameterValue(
            _('Invalid value for IP protocol. Unknown protocol.'))
    if (not isinstance(from_port, int) or
            not isinstance(to_port, int)):
        raise exception.InvalidParameterValue(
            _('Integer values should be specified for ports'))
    if protocol in ['tcp', 'udp', 6, 17]:
        if from_port == -1 or to_port == -1:
            raise exception.InvalidParameterValue(
                _('Must specify both from and to ports with TCP/UDP.'))
        if from_port > to_port:
            raise exception.InvalidParameterValue(
                _('Invalid TCP/UDP port range.'))
        if from_port < 0 or from_port > 65535:
            raise exception.InvalidParameterValue(
                _('TCP/UDP from port is out of range.'))
        if to_port < 0 or to_port > 65535:
            raise exception.InvalidParameterValue(
                _('TCP/UDP to port is out of range.'))
    elif protocol in ['icmp', 1]:
        if from_port < -1 or from_port > 255:
            raise exception.InvalidParameterValue(
                _('ICMP type is out of range.'))
        if to_port < -1 or to_port > 255:
            raise exception.InvalidParameterValue(
                _('ICMP code is out of range.'))


def _build_rules(context, group_id, group_name, ip_permissions, direction):
    if group_name is None and group_id is None:
        raise exception.MissingParameter(param='group id or name')
    if ip_permissions is None:
        raise exception.MissingParameter(param='source group or cidr')
    os_security_group_id = security_group_engine.get_group_os_id(context,
                                                                 group_id,
                                                                 group_name)
    os_security_group_rule_bodies = []
    if ip_permissions is None:
        ip_permissions = []
    for rule in ip_permissions:
        os_security_group_rule_body = (
            {'security_group_id': os_security_group_id,
             'direction': direction,
             'ethertype': 'IPv4'})
        protocol = rule.get('ip_protocol', -1)
        from_port = rule.get('from_port', -1)
        to_port = rule.get('to_port', -1)
        _validate_parameters(protocol, from_port, to_port)
        if protocol != -1:
            os_security_group_rule_body['protocol'] = rule['ip_protocol']
        if from_port != -1:
            os_security_group_rule_body['port_range_min'] = rule['from_port']
        if to_port != -1:
            os_security_group_rule_body['port_range_max'] = rule['to_port']
        # NOTE(Dmitry_Eremeev): Neutron behaviour changed.
        # If rule with full port range is created (1 - 65535), then Neutron
        # creates rule without ports specified.
        # If a rule with full port range must be deleted, then Neutron cannot
        # find a rule with this range in order to delete it, but it can find
        # a rule which has not ports in its properties.
        if ((from_port == 1) and (to_port in [255, 65535])):
            for item in ['port_range_min', 'port_range_max']:
                del os_security_group_rule_body[item]
        # TODO(Alex) AWS protocol claims support of multiple groups and cidrs,
        # however, neutron doesn't support it at the moment.
        # It's possible in the future to convert list values incoming from
        # REST API into several neutron rules and squeeze them back into one
        # for describing.
        # For now only 1 value is supported for either.
        if rule.get('groups'):
            os_security_group_rule_body['remote_group_id'] = (
                security_group_engine.get_group_os_id(
                    context,
                    rule['groups'][0].get('group_id'),
                    rule['groups'][0].get('group_name')))
        elif rule.get('ip_ranges'):
            os_security_group_rule_body['remote_ip_prefix'] = (
                rule['ip_ranges'][0]['cidr_ip'])
            validator.validate_cidr_with_ipv6(
                os_security_group_rule_body['remote_ip_prefix'], 'cidr_ip')
        else:
            raise exception.MissingParameter(param='source group or cidr')
        os_security_group_rule_bodies.append(os_security_group_rule_body)
    return os_security_group_rule_bodies


def revoke_security_group_ingress(context, group_id=None,
                                  group_name=None, ip_permissions=None):
    return _revoke_security_group(context, group_id, group_name,
                                  ip_permissions, 'ingress')


def revoke_security_group_egress(context, group_id, ip_permissions=None):
    security_group = ec2utils.get_db_item(context, group_id)
    if not security_group.get('vpc_id'):
        raise exception.InvalidParameterValue(message=_('Only Amazon VPC '
            'security groups may be used with this operation.'))
    return _revoke_security_group(context, group_id, None,
                                  ip_permissions, 'egress')


def _are_identical_rules(rule1, rule2):

    def significant_values(rule):
        dict = {}
        for key, value in rule.items():
            if (value is not None and value != -1 and
                    value != '0.0.0.0/0' and
                    key not in ['id', 'tenant_id', 'security_group_id', 'tags',
                                'description', 'revision', 'revision_number',
                                'created_at', 'updated_at', 'project_id']):
                dict[key] = str(value)
        return dict

    r1 = significant_values(rule1)
    r2 = significant_values(rule2)
    return r1 == r2


def _revoke_security_group(context, group_id, group_name, ip_permissions,
                           direction):
    rules_bodies = _build_rules(context, group_id, group_name,
                                ip_permissions, direction)
    if not rules_bodies:
        return True
    os_rules = security_group_engine.get_os_group_rules(
        context, rules_bodies[0]['security_group_id'])

    os_rules_to_delete = []
    for rule_body in rules_bodies:
        for os_rule in os_rules:
            if _are_identical_rules(rule_body, os_rule):
                os_rules_to_delete.append(os_rule['id'])

    if len(os_rules_to_delete) != len(rules_bodies):
        security_group = ec2utils.get_db_item(context, group_id)
        if security_group.get('vpc_id'):
            raise exception.InvalidPermissionNotFound()
        return True
    for os_rule_id in os_rules_to_delete:
        security_group_engine.delete_os_group_rule(context, os_rule_id)
    return True


def _translate_group_name(context, os_group, db_groups):
    # NOTE(Alex): This function translates VPC default group names
    # from vpc id 'vpc-xxxxxxxx' format to 'default'. It's supposed
    # to be called right after getting security groups from OpenStack
    # in order to avoid problems with incoming 'default' name value
    # in all of the subsequent handling (filtering, using in parameters...)
    if os_group['name'].startswith('vpc-') and db_groups:
        db_group = next((g for g in db_groups
                        if g['os_id'] == os_group['id']), None)
        if db_group and db_group.get('vpc_id'):
            return DEFAULT_GROUP_NAME
    return os_group['name']


def _format_security_groups_ids_names(context):
    neutron = clients.neutron(context)
    os_security_groups = neutron.list_security_groups(
        tenant_id=context.project_id)['security_groups']
    security_groups = db_api.get_items(context, 'sg')
    ec2_security_groups = {}
    for os_security_group in os_security_groups:
        security_group = next((g for g in security_groups
                               if g['os_id'] == os_security_group['id']), None)
        if security_group is None:
            continue
        ec2_security_groups[os_security_group['id']] = (
            {'groupId': security_group['id'],
             'groupName': _translate_group_name(context,
                                                os_security_group,
                                                security_groups)})
    return ec2_security_groups


def _format_security_group(security_group, os_security_group,
                           security_groups, os_security_groups):
    ec2_security_group = {}
    ec2_security_group['groupId'] = security_group['id']
    if security_group.get('vpc_id'):
        ec2_security_group['vpcId'] = security_group['vpc_id']
    ec2_security_group['ownerId'] = os_security_group['tenant_id']
    ec2_security_group['groupName'] = os_security_group['name']
    ec2_security_group['groupDescription'] = os_security_group['description']
    ingress_permissions = []
    egress_permissions = []
    for os_rule in os_security_group.get('security_group_rules', []):
        # NOTE(Alex) We're skipping IPv6 rules because AWS doesn't support
        # them.
        if os_rule.get('ethertype', 'IPv4') == 'IPv6':
            continue
        # NOTE(Dmitry_Eremeev): Neutron behaviour changed.
        # If rule with full port range (except icmp protocol) is created
        # (1 - 65535), then Neutron creates rule without ports specified.
        # Ports passed for rule creation don't match ports in created rule.
        # That's why default values were changed to match full port
        # range (1 - 65535)
        if os_rule.get('protocol') in ["icmp", 1]:
            min_port = max_port = -1
        else:
            min_port = 1
            max_port = 65535
        ec2_rule = {'ipProtocol': -1 if os_rule['protocol'] is None
                    else os_rule['protocol'],
                    'fromPort': min_port if os_rule['port_range_min'] is None
                    else os_rule['port_range_min'],
                    'toPort': max_port if os_rule['port_range_max'] is None
                    else os_rule['port_range_max']}
        remote_group_id = os_rule['remote_group_id']
        if remote_group_id is not None:
            ec2_remote_group = {}
            db_remote_group = next((g for g in security_groups
                                    if g['os_id'] == remote_group_id), None)
            if db_remote_group is not None:
                ec2_remote_group['groupId'] = db_remote_group['id']
            else:
                # TODO(Alex) Log absence of remote_group
                pass
            os_remote_group = next((g for g in os_security_groups
                                    if g['id'] == remote_group_id), None)
            if os_remote_group is not None:
                ec2_remote_group['groupName'] = os_remote_group['name']
                ec2_remote_group['userId'] = os_remote_group['tenant_id']
            else:
                # TODO(Alex) Log absence of remote_group
                pass
            ec2_rule['groups'] = [ec2_remote_group]
        elif os_rule['remote_ip_prefix'] is not None:
            ec2_rule['ipRanges'] = [{'cidrIp': os_rule['remote_ip_prefix']}]
        if os_rule.get('direction') == 'egress':
            egress_permissions.append(ec2_rule)
        else:
            if security_group is None and os_rule['protocol'] is None:
                for protocol, min_port, max_port in (('icmp', -1, -1),
                                                     ('tcp', 1, 65535),
                                                     ('udp', 1, 65535)):
                    ec2_rule['ipProtocol'] = protocol
                    ec2_rule['fromPort'] = min_port
                    ec2_rule['toPort'] = max_port
                    ingress_permissions.append(copy.deepcopy(ec2_rule))
            else:
                ingress_permissions.append(ec2_rule)

    ec2_security_group['ipPermissions'] = ingress_permissions
    ec2_security_group['ipPermissionsEgress'] = egress_permissions
    return ec2_security_group


class SecurityGroupEngineNeutron(object):

    def delete_group(self, context, group_name=None, group_id=None,
                     delete_default=False):
        neutron = clients.neutron(context)
        if group_name:
            sg = describe_security_groups(
                context,
                group_name=[group_name])['securityGroupInfo'][0]
            group_id = sg['groupId']
            group_name = None

        security_group = ec2utils.get_db_item(context, group_id)
        try:
            if not delete_default:
                os_security_group = neutron.show_security_group(
                    security_group['os_id'])
                if (os_security_group and
                    os_security_group['security_group']['name'] ==
                        security_group['vpc_id']):
                    raise exception.CannotDelete()
            neutron.delete_security_group(security_group['os_id'])
        except neutron_exception.Conflict as ex:
            # TODO(Alex): Instance ID is unknown here, report exception message
            # in its place - looks readable.
            raise exception.DependencyViolation(
                        obj1_id=group_id,
                        obj2_id=ex.message)
        except neutron_exception.NeutronClientException as ex:
            # TODO(Alex): do log error
            # TODO(Alex): adjust caught exception classes to catch:
            # the port doesn't exist
            pass
        db_api.delete_item(context, group_id)

    def get_os_groups(self, context):
        neutron = clients.neutron(context)
        return neutron.list_security_groups(
            tenant_id=context.project_id)['security_groups']

    def authorize_security_group(self, context, rule_body):
        neutron = clients.neutron(context)
        try:
            os_security_group_rule = neutron.create_security_group_rule(
                {'security_group_rule': rule_body})['security_group_rule']
        except neutron_exception.OverQuotaClient:
            raise exception.RulesPerSecurityGroupLimitExceeded()
        except neutron_exception.Conflict as ex:
            raise exception.InvalidPermissionDuplicate()

    def get_os_group_rules(self, context, os_id):
        neutron = clients.neutron(context)
        os_security_group = (
            neutron.show_security_group(os_id)['security_group'])
        return os_security_group.get('security_group_rules')

    def delete_os_group_rule(self, context, os_id):
        neutron = clients.neutron(context)
        neutron.delete_security_group_rule(os_id)

    def get_group_os_id(self, context, group_id, group_name):
        if group_name and not group_id:
            os_group = self.get_os_group_by_name(context, group_name)
            return str(os_group['id'])
        return ec2utils.get_db_item(context, group_id, 'sg')['os_id']

    def get_os_group_by_name(self, context, group_name,
                               os_security_groups=None):
        if os_security_groups is None:
            neutron = clients.neutron(context)
            os_security_groups = (
                neutron.list_security_groups()['security_groups'])
        os_group = next((g for g in os_security_groups
                           if g['name'] == group_name), None)
        if os_group is None:
            raise exception.InvalidGroupNotFound(id=group_name)
        return os_group


security_group_engine = get_security_group_engine()
