# -*- coding: utf-8 -*-
# Copyright (c) 2018 Tigera, Inc. 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.
# 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 neutron_lib.constants import IP_PROTOCOL_MAP

from oslo_log import log

from networking_calico import datamodel_v3
from networking_calico.common import config as calico_config
from networking_calico.plugins.ml2.drivers.calico.syncer import ResourceSyncer

LOG = log.getLogger(__name__)

# Each OpenStack security group is mapped to a Calico NetworkPolicy.  A VM's
# security group membership is represented by the VM having a label for each
# security group that it belongs to; thus the selector
# 'has(<security-group-label>)' represents the VMs that belong to that security
# group.
#
# The label for each security group is 'sg.projectcalico.org/openstack-'
# followed by the security group ID, and the name of the NetworkPolicy for each
# security group is 'ossg.default.'  followed by the security group ID.
SG_LABEL_PREFIX = "sg.projectcalico.org/openstack-"
SG_NAME_LABEL_PREFIX = "sg-name.projectcalico.org/openstack-"
SG_NAME_MAX_LENGTH = datamodel_v3.SANITIZE_LABEL_MAX_LENGTH - len(SG_NAME_LABEL_PREFIX)
SG_NAME_PREFIX = "ossg.default."


class PolicySyncer(ResourceSyncer):

    def __init__(self, db, txn_from_context):
        super(PolicySyncer, self).__init__(db, txn_from_context, "NetworkPolicy")
        self.region_string = calico_config.get_region_string()
        self.namespace = datamodel_v3.get_namespace(self.region_string)

    def delete_legacy_etcd_data(self):
        if self.namespace != datamodel_v3.NO_REGION_NAMESPACE:
            datamodel_v3.delete_legacy(self.resource_kind, SG_NAME_PREFIX)

    def get_all_from_etcd(self):
        results = []
        for r in datamodel_v3.get_all(self.resource_kind, self.namespace):
            name, _, _ = r
            if name.startswith(SG_NAME_PREFIX):
                results.append(r)
        return results

    def create_in_etcd(self, name, spec):
        return datamodel_v3.put(
            self.resource_kind, self.namespace, name, spec, mod_revision=0
        )

    def update_in_etcd(self, name, spec, mod_revision=None):
        return datamodel_v3.put(
            self.resource_kind, self.namespace, name, spec, mod_revision=mod_revision
        )

    def delete_from_etcd(self, name, mod_revision):
        return datamodel_v3.delete(
            self.resource_kind, self.namespace, name, mod_revision=mod_revision
        )

    def get_all_from_neutron(self, context):
        return dict(
            (SG_NAME_PREFIX + sg["id"], sg)
            for sg in self.db.get_security_groups(context)
        )

    def neutron_to_etcd_write_data(self, sg, context, reread=False):
        if reread:
            # We don't need to reread the SG row itself here, because we don't
            # use any information from it, apart from its ID as a key for the
            # following rules.
            pass
        rules = self.db.get_security_group_rules(
            context, filters={"security_group_id": [sg["id"]]}
        )
        return policy_spec(sg["id"], rules)

    def write_sgs_to_etcd(self, sgids, context):
        rules = self.db.get_security_group_rules(
            context, filters={"security_group_id": sgids}
        )
        for sgid in sgids:
            self.update_in_etcd(SG_NAME_PREFIX + sgid, policy_spec(sgid, rules))


def policy_spec(sgid, rules):
    """Generate JSON NetworkPolicySpec for the given security group."""

    # <rules> can include those for several security groups.  Pick out the
    # rules for the security group that we are translating right now.
    sg_rules = (r for r in rules if r["security_group_id"] == sgid)

    # Split the rules based on direction, and map to Calico form.
    inbound_rules = []
    outbound_rules = []
    for rule in sg_rules:
        if rule["direction"] == "ingress":
            inbound_rules.append(_neutron_rule_to_etcd_rule(rule))
        else:
            outbound_rules.append(_neutron_rule_to_etcd_rule(rule))

    return {
        "ingress": inbound_rules,
        "egress": outbound_rules,
        "selector": "has(%s)" % (SG_LABEL_PREFIX + sgid),
    }


def _neutron_rule_to_etcd_rule(rule):
    """_neutron_rule_to_etcd_rule

    Translate a single Neutron rule dict to a single dict in our
    etcd format.
    """
    ethertype = rule["ethertype"]
    etcd_rule = {"action": "Allow"}

    # Map the ethertype field from Neutron to etcd format.
    etcd_rule["ipVersion"] = {"IPv4": 4, "IPv6": 6}[ethertype]

    # Map the protocol field from Neutron to etcd format.
    #
    # Per https://docs.openstack.org/api-ref/network/v2/
    # ?expanded=create-security-group-rule-detail#
    # security-group-rules-security-group-rules:
    #
    # "The IP protocol can be represented by a string, an integer, or
    # null. Valid string or integer values are any or 0, ah or 51,
    # dccp or 33, egp or 8, esp or 50, gre or 47, icmp or 1, icmpv6 or
    # 58, igmp or 2, ipip or 4, ipv6-encap or 41, ipv6-frag or 44,
    # ipv6-icmp or 58, ipv6-nonxt or 59, ipv6-opts or 60, ipv6-route
    # or 43, ospf or 89, pgm or 113, rsvp or 46, sctp or 132, tcp or
    # 6, udp or 17, udplite or 136, vrrp or 112. Additionally, any
    # integer value between [0-255] is also valid. The string any (or
    # integer 0) means all IP protocols. See the constants in
    # neutron_lib.constants for the most up-to-date list of supported
    # strings."
    #
    # In addition we have previously supported -1 for any, and 'icmp'
    # to mean 'icmpv6' when the ethertype is IPv6; so we also retain
    # those for back-compatibility.
    if rule["protocol"] is None or rule["protocol"] in [-1, 0, "any"]:
        # In the Calico data model, 'any' is represented by omitting
        # the 'protocol' field.
        pass
    elif rule["protocol"] == "ipv6-icmp":
        etcd_rule["protocol"] = "ICMPv6"
    elif rule["protocol"] == "icmp":
        etcd_rule["protocol"] = {"IPv4": "ICMP", "IPv6": "ICMPv6"}[ethertype]
    elif isinstance(rule["protocol"], int):
        etcd_rule["protocol"] = rule["protocol"]
    elif rule["protocol"] in IP_PROTOCOL_MAP:
        etcd_rule["protocol"] = IP_PROTOCOL_MAP[rule["protocol"]]
    else:
        etcd_rule["protocol"] = rule["protocol"].upper()

    port_spec = None
    if rule["protocol"] == "icmp" or rule["protocol"] == "ipv6-icmp":
        # OpenStack stashes the ICMP match criteria in
        # port_range_min/max.
        icmp_fields = {}
        icmp_type = rule["port_range_min"]
        if icmp_type is not None and icmp_type != -1:
            icmp_fields["type"] = icmp_type
        icmp_code = rule["port_range_max"]
        if icmp_code is not None and icmp_code != -1:
            icmp_fields["code"] = icmp_code
        if icmp_fields:
            etcd_rule["icmp"] = icmp_fields
    else:
        # src/dst_ports is a list in which each entry can be a
        # single number, or a string describing a port range.
        if rule["port_range_min"] == -1:
            port_spec = None
        elif rule["port_range_min"] == rule["port_range_max"]:
            if rule["port_range_min"] is not None:
                port_spec = [rule["port_range_min"]]
        else:
            port_spec = ["%s:%s" % (rule["port_range_min"], rule["port_range_max"])]

    entity_rule = {}
    if rule["remote_group_id"] is not None:
        entity_rule["selector"] = "has(%s)" % (
            SG_LABEL_PREFIX + rule["remote_group_id"]
        )
    if rule["remote_ip_prefix"] is not None:
        entity_rule["nets"] = [rule["remote_ip_prefix"]]

    if port_spec is not None:
        if rule["direction"] == "ingress":
            etcd_rule["destination"] = {"ports": port_spec}
        else:
            entity_rule["ports"] = port_spec

    # Store in source or destination field of the overall rule.
    if entity_rule:
        if rule["direction"] == "ingress":
            etcd_rule["source"] = entity_rule
        else:
            etcd_rule["destination"] = entity_rule

    LOG.debug("=> %s Calico rule %s" % (rule["direction"], etcd_rule))

    return etcd_rule
