# vim: tabstop=4 shiftwidth=4 softtabstop=4

# Copyright (C) 2012 Midokura Japan K.K.
# Copyright (C) 2013 Midokura PTE LTD
# 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.
#
# @author: Tomoe Sugihara, Midokura Japan KK
# @author: Ryu Ishimoto, Midokura Japan KK
# @author: Rossella Sblendido, Midokura Japan KK


from midonetclient import exc
from webob import exc as w_exc

from neutron.common import exceptions as n_exc
from neutron.openstack.common import log as logging
from neutron.plugins.midonet.common import net_util

LOG = logging.getLogger(__name__)


def handle_api_error(fn):
    """Wrapper for methods that throws custom exceptions."""
    def wrapped(*args, **kwargs):
        try:
            return fn(*args, **kwargs)
        except (w_exc.HTTPException,
                exc.MidoApiConnectionError) as ex:
            raise MidonetApiException(msg=ex)
    return wrapped


class MidonetResourceNotFound(n_exc.NotFound):
    message = _('MidoNet %(resource_type)s %(id)s could not be found')


class MidonetApiException(n_exc.NeutronException):
    message = _("MidoNet API error: %(msg)s")


class MidoClient:

    def __init__(self, mido_api):
        self.mido_api = mido_api

    @handle_api_error
    def create_bridge(self, tenant_id, name):
        """Create a new bridge

        :param tenant_id: id of tenant creating the bridge
        :param name: name of the bridge
        :returns: newly created bridge
        """
        LOG.debug(_("MidoClient.create_bridge called: "
                    "tenant_id=%(tenant_id)s, name=%(name)s"),
                  {'tenant_id': tenant_id, 'name': name})
        return self.mido_api.add_bridge().name(name).tenant_id(
            tenant_id).create()

    @handle_api_error
    def delete_bridge(self, id):
        """Delete a bridge

        :param id: id of the bridge
        """
        LOG.debug(_("MidoClient.delete_bridge called: id=%(id)s"), {'id': id})
        return self.mido_api.delete_bridge(id)

    @handle_api_error
    def get_bridge(self, id):
        """Get a bridge

        :param id: id of the bridge
        :returns: requested bridge. None if bridge does not exist.
        """
        LOG.debug(_("MidoClient.get_bridge called: id=%s"), id)
        try:
            return self.mido_api.get_bridge(id)
        except w_exc.HTTPNotFound:
            raise MidonetResourceNotFound(resource_type='Bridge', id=id)

    @handle_api_error
    def update_bridge(self, id, name):
        """Update a bridge of the given id with the new name

        :param id: id of the bridge
        :param name: name of the bridge to set to
        :returns: bridge object
        """
        LOG.debug(_("MidoClient.update_bridge called: "
                    "id=%(id)s, name=%(name)s"), {'id': id, 'name': name})
        try:
            return self.mido_api.get_bridge(id).name(name).update()
        except w_exc.HTTPNotFound:
            raise MidonetResourceNotFound(resource_type='Bridge', id=id)

    @handle_api_error
    def create_dhcp(self, bridge, gateway_ip, cidr, host_rts=None,
                    dns_servers=None):
        """Create a new DHCP entry

        :param bridge: bridge object to add dhcp to
        :param gateway_ip: IP address of gateway
        :param cidr: subnet represented as x.x.x.x/y
        :param host_rts: list of routes set in the host
        :param dns_servers: list of dns servers
        :returns: newly created dhcp
        """
        LOG.debug(_("MidoClient.create_dhcp called: bridge=%(bridge)s, "
                    "cidr=%(cidr)s, gateway_ip=%(gateway_ip)s, "
                    "host_rts=%(host_rts)s, dns_servers=%(dns_servers)s"),
                  {'bridge': bridge, 'cidr': cidr, 'gateway_ip': gateway_ip,
                   'host_rts': host_rts, 'dns_servers': dns_servers})
        self.mido_api.add_bridge_dhcp(bridge, gateway_ip, cidr,
                                      host_rts=host_rts,
                                      dns_nservers=dns_servers)

    @handle_api_error
    def add_dhcp_host(self, bridge, cidr, ip, mac):
        """Add DHCP host entry

        :param bridge: bridge the DHCP is configured for
        :param cidr: subnet represented as x.x.x.x/y
        :param ip: IP address
        :param mac: MAC address
        """
        LOG.debug(_("MidoClient.add_dhcp_host called: bridge=%(bridge)s, "
                    "cidr=%(cidr)s, ip=%(ip)s, mac=%(mac)s"),
                  {'bridge': bridge, 'cidr': cidr, 'ip': ip, 'mac': mac})
        subnet = bridge.get_dhcp_subnet(net_util.subnet_str(cidr))
        if subnet is None:
            raise MidonetApiException(msg=_("Tried to add to"
                                            "non-existent DHCP"))

        subnet.add_dhcp_host().ip_addr(ip).mac_addr(mac).create()

    @handle_api_error
    def remove_dhcp_host(self, bridge, cidr, ip, mac):
        """Remove DHCP host entry

        :param bridge: bridge the DHCP is configured for
        :param cidr: subnet represented as x.x.x.x/y
        :param ip: IP address
        :param mac: MAC address
        """
        LOG.debug(_("MidoClient.remove_dhcp_host called: bridge=%(bridge)s, "
                    "cidr=%(cidr)s, ip=%(ip)s, mac=%(mac)s"),
                  {'bridge': bridge, 'cidr': cidr, 'ip': ip, 'mac': mac})
        subnet = bridge.get_dhcp_subnet(net_util.subnet_str(cidr))
        if subnet is None:
            LOG.warn(_("Tried to delete mapping from non-existent subnet"))
            return

        for dh in subnet.get_dhcp_hosts():
            if dh.get_mac_addr() == mac and dh.get_ip_addr() == ip:
                LOG.debug(_("MidoClient.remove_dhcp_host: Deleting %(dh)r"),
                          {"dh": dh})
                dh.delete()

    @handle_api_error
    def delete_dhcp_host(self, bridge_id, cidr, ip, mac):
        """Delete DHCP host entry

        :param bridge_id: id of the bridge of the DHCP
        :param cidr: subnet represented as x.x.x.x/y
        :param ip: IP address
        :param mac: MAC address
        """
        LOG.debug(_("MidoClient.delete_dhcp_host called: "
                    "bridge_id=%(bridge_id)s, cidr=%(cidr)s, ip=%(ip)s, "
                    "mac=%(mac)s"), {'bridge_id': bridge_id,
                                     'cidr': cidr,
                                     'ip': ip, 'mac': mac})
        bridge = self.get_bridge(bridge_id)
        self.remove_dhcp_host(bridge, net_util.subnet_str(cidr), ip, mac)

    @handle_api_error
    def delete_dhcp(self, bridge):
        """Delete a DHCP entry

        :param bridge: bridge to remove DHCP from
        """
        LOG.debug(_("MidoClient.delete_dhcp called: bridge=%(bridge)s, "),
                  {'bridge': bridge})
        dhcp = bridge.get_dhcp_subnets()
        if not dhcp:
            raise MidonetApiException(
                msg=_("Tried to delete non-existent DHCP"))
        dhcp[0].delete()

    @handle_api_error
    def delete_port(self, id, delete_chains=False):
        """Delete a port

        :param id: id of the port
        """
        LOG.debug(_("MidoClient.delete_port called: id=%(id)s, "
                    "delete_chains=%(delete_chains)s"),
                  {'id': id, 'delete_chains': delete_chains})
        if delete_chains:
            self.delete_port_chains(id)

        self.mido_api.delete_port(id)

    @handle_api_error
    def get_port(self, id):
        """Get a port

        :param id: id of the port
        :returns: requested port. None if it does not exist
        """
        LOG.debug(_("MidoClient.get_port called: id=%(id)s"), {'id': id})
        try:
            return self.mido_api.get_port(id)
        except w_exc.HTTPNotFound:
            raise MidonetResourceNotFound(resource_type='Port', id=id)

    @handle_api_error
    def add_bridge_port(self, bridge):
        """Add a port on a bridge

        :param bridge: Bridge to add a new port to
        :returns: newly created port
        """
        LOG.debug(_("MidoClient.add_bridge_port called: "
                    "bridge=%(bridge)s"), {'bridge': bridge})
        return self.mido_api.add_bridge_port(bridge)

    @handle_api_error
    def add_router_port(self, router, port_address=None,
                        network_address=None, network_length=None):
        """Add a new port to an existing router."""
        return self.mido_api.add_router_port(router,
                                             port_address=port_address,
                                             network_address=network_address,
                                             network_length=network_length)

    @handle_api_error
    def create_router(self, tenant_id, name):
        """Create a new router

        :param tenant_id: id of tenant creating the router
        :param name: name of the router
        :returns: newly created router
        """
        LOG.debug(_("MidoClient.create_router called: "
                    "tenant_id=%(tenant_id)s, name=%(name)s"),
                  {'tenant_id': tenant_id, 'name': name})
        return self.mido_api.add_router().name(name).tenant_id(
            tenant_id).create()

    @handle_api_error
    def delete_router(self, id):
        """Delete a router

        :param id: id of the router
        """
        LOG.debug(_("MidoClient.delete_router called: id=%(id)s"), {'id': id})
        return self.mido_api.delete_router(id)

    @handle_api_error
    def get_router(self, id):
        """Get a router with the given id

        :param id: id of the router
        :returns: requested router object.  None if it does not exist.
        """
        LOG.debug(_("MidoClient.get_router called: id=%(id)s"), {'id': id})
        try:
            return self.mido_api.get_router(id)
        except w_exc.HTTPNotFound:
            raise MidonetResourceNotFound(resource_type='Router', id=id)

    @handle_api_error
    def update_router(self, id, name):
        """Update a router of the given id with the new name

        :param id: id of the router
        :param name: name of the router to set to
        :returns: router object
        """
        LOG.debug(_("MidoClient.update_router called: "
                    "id=%(id)s, name=%(name)s"), {'id': id, 'name': name})
        try:
            return self.mido_api.get_router(id).name(name).update()
        except w_exc.HTTPNotFound:
            raise MidonetResourceNotFound(resource_type='Router', id=id)

    @handle_api_error
    def delete_route(self, id):
        return self.mido_api.delete_route(id)

    @handle_api_error
    def add_dhcp_route_option(self, bridge, cidr, gw_ip, dst_ip):
        """Add Option121 route to subnet

        :param bridge: Bridge to add the option route to
        :param cidr: subnet represented as x.x.x.x/y
        :param gw_ip: IP address of the next hop
        :param dst_ip: IP address of the destination, in x.x.x.x/y format
        """
        LOG.debug(_("MidoClient.add_dhcp_route_option called: "
                    "bridge=%(bridge)s, cidr=%(cidr)s, gw_ip=%(gw_ip)s"
                    "dst_ip=%(dst_ip)s"),
                  {"bridge": bridge, "cidr": cidr, "gw_ip": gw_ip,
                   "dst_ip": dst_ip})
        subnet = bridge.get_dhcp_subnet(net_util.subnet_str(cidr))
        if subnet is None:
            raise MidonetApiException(
                msg=_("Tried to access non-existent DHCP"))
        prefix, length = dst_ip.split("/")
        routes = [{'destinationPrefix': prefix, 'destinationLength': length,
                   'gatewayAddr': gw_ip}]
        cur_routes = subnet.get_opt121_routes()
        if cur_routes:
            routes = routes + cur_routes
        subnet.opt121_routes(routes).update()

    @handle_api_error
    def link(self, port, peer_id):
        """Link a port to a given peerId."""
        self.mido_api.link(port, peer_id)

    @handle_api_error
    def delete_port_routes(self, routes, port_id):
        """Remove routes whose next hop port is the given port ID."""
        for route in routes:
            if route.get_next_hop_port() == port_id:
                self.mido_api.delete_route(route.get_id())

    @handle_api_error
    def get_router_routes(self, router_id):
        """Get all routes for the given router."""
        return self.mido_api.get_router_routes(router_id)

    @handle_api_error
    def unlink(self, port):
        """Unlink a port

        :param port: port object
        """
        LOG.debug(_("MidoClient.unlink called: port=%(port)s"),
                  {'port': port})
        if port.get_peer_id():
            self.mido_api.unlink(port)
        else:
            LOG.warn(_("Attempted to unlink a port that was not linked. %s"),
                     port.get_id())

    @handle_api_error
    def remove_rules_by_property(self, tenant_id, chain_name, key, value):
        """Remove all the rules that match the provided key and value."""
        LOG.debug(_("MidoClient.remove_rules_by_property called: "
                    "tenant_id=%(tenant_id)s, chain_name=%(chain_name)s"
                    "key=%(key)s, value=%(value)s"),
                  {'tenant_id': tenant_id, 'chain_name': chain_name,
                   'key': key, 'value': value})
        chain = self.get_chain_by_name(tenant_id, chain_name)
        if chain is None:
            raise MidonetResourceNotFound(resource_type='Chain',
                                          id=chain_name)

        for r in chain.get_rules():
            if key in r.get_properties():
                if r.get_properties()[key] == value:
                    self.mido_api.delete_rule(r.get_id())

    @handle_api_error
    def add_router_chains(self, router, inbound_chain_name,
                          outbound_chain_name):
        """Create chains for a new router.

        Creates inbound and outbound chains for the router with the given
        names, and the new chains are set on the router.

        :param router: router to set chains for
        :param inbound_chain_name: Name of the inbound chain
        :param outbound_chain_name: Name of the outbound chain
        """
        LOG.debug(_("MidoClient.create_router_chains called: "
                    "router=%(router)s, inbound_chain_name=%(in_chain)s, "
                    "outbound_chain_name=%(out_chain)s"),
                  {"router": router, "in_chain": inbound_chain_name,
                   "out_chain": outbound_chain_name})
        tenant_id = router.get_tenant_id()

        inbound_chain = self.mido_api.add_chain().tenant_id(tenant_id).name(
            inbound_chain_name,).create()
        outbound_chain = self.mido_api.add_chain().tenant_id(tenant_id).name(
            outbound_chain_name).create()

        # set chains to in/out filters
        router.inbound_filter_id(inbound_chain.get_id()).outbound_filter_id(
            outbound_chain.get_id()).update()
        return inbound_chain, outbound_chain

    @handle_api_error
    def delete_router_chains(self, id):
        """Deletes chains of a router.

        :param id: router ID to delete chains of
        """
        LOG.debug(_("MidoClient.delete_router_chains called: "
                    "id=%(id)s"), {'id': id})
        router = self.get_router(id)
        if (router.get_inbound_filter_id()):
            self.mido_api.delete_chain(router.get_inbound_filter_id())

        if (router.get_outbound_filter_id()):
            self.mido_api.delete_chain(router.get_outbound_filter_id())

    @handle_api_error
    def delete_port_chains(self, id):
        """Deletes chains of a port.

        :param id: port ID to delete chains of
        """
        LOG.debug(_("MidoClient.delete_port_chains called: "
                    "id=%(id)s"), {'id': id})
        port = self.get_port(id)
        if (port.get_inbound_filter_id()):
            self.mido_api.delete_chain(port.get_inbound_filter_id())

        if (port.get_outbound_filter_id()):
            self.mido_api.delete_chain(port.get_outbound_filter_id())

    @handle_api_error
    def get_link_port(self, router, peer_router_id):
        """Setup a route on the router to the next hop router."""
        LOG.debug(_("MidoClient.get_link_port called: "
                    "router=%(router)s, peer_router_id=%(peer_router_id)s"),
                  {'router': router, 'peer_router_id': peer_router_id})
        # Find the port linked between the two routers
        link_port = None
        for p in router.get_peer_ports():
            if p.get_device_id() == peer_router_id:
                link_port = p
                break
        return link_port

    @handle_api_error
    def add_router_route(self, router, type='Normal',
                         src_network_addr=None, src_network_length=None,
                         dst_network_addr=None, dst_network_length=None,
                         next_hop_port=None, next_hop_gateway=None,
                         weight=100):
        """Setup a route on the router."""
        return self.mido_api.add_router_route(
            router, type=type, src_network_addr=src_network_addr,
            src_network_length=src_network_length,
            dst_network_addr=dst_network_addr,
            dst_network_length=dst_network_length,
            next_hop_port=next_hop_port, next_hop_gateway=next_hop_gateway,
            weight=weight)

    @handle_api_error
    def add_static_nat(self, tenant_id, chain_name, from_ip, to_ip, port_id,
                       nat_type='dnat', **kwargs):
        """Add a static NAT entry

        :param tenant_id: owner fo the chain to add a NAT to
        :param chain_name: name of the chain to add a NAT to
        :param from_ip: IP to translate from
        :param from_ip: IP to translate from
        :param to_ip: IP to translate to
        :param port_id: port to match on
        :param nat_type: 'dnat' or 'snat'
        """
        LOG.debug(_("MidoClient.add_static_nat called: "
                    "tenant_id=%(tenant_id)s, chain_name=%(chain_name)s, "
                    "from_ip=%(from_ip)s, to_ip=%(to_ip)s, "
                    "port_id=%(port_id)s, nat_type=%(nat_type)s"),
                  {'tenant_id': tenant_id, 'chain_name': chain_name,
                   'from_ip': from_ip, 'to_ip': to_ip,
                   'portid': port_id, 'nat_type': nat_type})
        if nat_type not in ['dnat', 'snat']:
            raise ValueError(_("Invalid NAT type passed in %s") % nat_type)

        chain = self.get_chain_by_name(tenant_id, chain_name)
        nat_targets = []
        nat_targets.append(
            {'addressFrom': to_ip, 'addressTo': to_ip,
             'portFrom': 0, 'portTo': 0})

        rule = chain.add_rule().type(nat_type).flow_action('accept').position(
            1).nat_targets(nat_targets).properties(kwargs)

        if nat_type == 'dnat':
            rule = rule.nw_dst_address(from_ip).nw_dst_length(32).in_ports(
                [port_id])
        else:
            rule = rule.nw_src_address(from_ip).nw_src_length(32).out_ports(
                [port_id])

        return rule.create()

    @handle_api_error
    def add_dynamic_snat(self, tenant_id, pre_chain_name, post_chain_name,
                         snat_ip, port_id, **kwargs):
        """Add SNAT masquerading rule

        MidoNet requires two rules on the router, one to do NAT to a range of
        ports, and another to retrieve back the original IP in the return
        flow.
        """
        pre_chain = self.get_chain_by_name(tenant_id, pre_chain_name)
        post_chain = self.get_chain_by_name(tenant_id, post_chain_name)

        pre_chain.add_rule().nw_dst_address(snat_ip).nw_dst_length(
            32).type('rev_snat').flow_action('accept').in_ports(
                [port_id]).properties(kwargs).position(1).create()

        nat_targets = []
        nat_targets.append(
            {'addressFrom': snat_ip, 'addressTo': snat_ip,
             'portFrom': 1, 'portTo': 65535})

        post_chain.add_rule().type('snat').flow_action(
            'accept').nat_targets(nat_targets).out_ports(
                [port_id]).properties(kwargs).position(1).create()

    @handle_api_error
    def remove_static_route(self, router, ip):
        """Remove static route for the IP

        :param router: next hop router to remove the routes to
        :param ip: IP address of the route to remove
        """
        LOG.debug(_("MidoClient.remote_static_route called: "
                    "router=%(router)s, ip=%(ip)s"),
                  {'router': router, 'ip': ip})
        for r in router.get_routes():
            if (r.get_dst_network_addr() == ip and
                    r.get_dst_network_length() == 32):
                self.mido_api.delete_route(r.get_id())

    @handle_api_error
    def update_port_chains(self, port, inbound_chain_id, outbound_chain_id):
        """Bind inbound and outbound chains to the port."""
        LOG.debug(_("MidoClient.update_port_chains called: port=%(port)s"
                    "inbound_chain_id=%(inbound_chain_id)s, "
                    "outbound_chain_id=%(outbound_chain_id)s"),
                  {"port": port, "inbound_chain_id": inbound_chain_id,
                   "outbound_chain_id": outbound_chain_id})
        port.inbound_filter_id(inbound_chain_id).outbound_filter_id(
            outbound_chain_id).update()

    @handle_api_error
    def create_chain(self, tenant_id, name):
        """Create a new chain."""
        LOG.debug(_("MidoClient.create_chain called: tenant_id=%(tenant_id)s "
                    " name=%(name)s"), {"tenant_id": tenant_id, "name": name})
        return self.mido_api.add_chain().tenant_id(tenant_id).name(
            name).create()

    @handle_api_error
    def delete_chain(self, id):
        """Delete chain matching the ID."""
        LOG.debug(_("MidoClient.delete_chain called: id=%(id)s"), {"id": id})
        self.mido_api.delete_chain(id)

    @handle_api_error
    def delete_chains_by_names(self, tenant_id, names):
        """Delete chains matching the names given for a tenant."""
        LOG.debug(_("MidoClient.delete_chains_by_names called: "
                    "tenant_id=%(tenant_id)s names=%(names)s "),
                  {"tenant_id": tenant_id, "names": names})
        chains = self.mido_api.get_chains({'tenant_id': tenant_id})
        for c in chains:
            if c.get_name() in names:
                self.mido_api.delete_chain(c.get_id())

    @handle_api_error
    def get_chain_by_name(self, tenant_id, name):
        """Get the chain by its name."""
        LOG.debug(_("MidoClient.get_chain_by_name called: "
                    "tenant_id=%(tenant_id)s name=%(name)s "),
                  {"tenant_id": tenant_id, "name": name})
        for c in self.mido_api.get_chains({'tenant_id': tenant_id}):
            if c.get_name() == name:
                return c
        return None

    @handle_api_error
    def get_port_group_by_name(self, tenant_id, name):
        """Get the port group by name."""
        LOG.debug(_("MidoClient.get_port_group_by_name called: "
                    "tenant_id=%(tenant_id)s name=%(name)s "),
                  {"tenant_id": tenant_id, "name": name})
        for p in self.mido_api.get_port_groups({'tenant_id': tenant_id}):
            if p.get_name() == name:
                return p
        return None

    @handle_api_error
    def create_port_group(self, tenant_id, name):
        """Create a port group

        Create a new port group for a given name and ID.
        """
        LOG.debug(_("MidoClient.create_port_group called: "
                    "tenant_id=%(tenant_id)s name=%(name)s"),
                  {"tenant_id": tenant_id, "name": name})
        return self.mido_api.add_port_group().tenant_id(tenant_id).name(
            name).create()

    @handle_api_error
    def delete_port_group_by_name(self, tenant_id, name):
        """Delete port group matching the name given for a tenant."""
        LOG.debug(_("MidoClient.delete_port_group_by_name called: "
                    "tenant_id=%(tenant_id)s name=%(name)s "),
                  {"tenant_id": tenant_id, "name": name})
        pgs = self.mido_api.get_port_groups({'tenant_id': tenant_id})
        for pg in pgs:
            if pg.get_name() == name:
                LOG.debug(_("Deleting pg %(id)s"), {"id": pg.get_id()})
                self.mido_api.delete_port_group(pg.get_id())

    @handle_api_error
    def add_port_to_port_group_by_name(self, tenant_id, name, port_id):
        """Add a port to a port group with the given name."""
        LOG.debug(_("MidoClient.add_port_to_port_group_by_name called: "
                    "tenant_id=%(tenant_id)s name=%(name)s "
                    "port_id=%(port_id)s"),
                  {"tenant_id": tenant_id, "name": name, "port_id": port_id})
        pg = self.get_port_group_by_name(tenant_id, name)
        if pg is None:
            raise MidonetResourceNotFound(resource_type='PortGroup', id=name)

        pg = pg.add_port_group_port().port_id(port_id).create()
        return pg

    @handle_api_error
    def remove_port_from_port_groups(self, port_id):
        """Remove a port binding from all the port groups."""
        LOG.debug(_("MidoClient.remove_port_from_port_groups called: "
                    "port_id=%(port_id)s"), {"port_id": port_id})
        port = self.get_port(port_id)
        for pg in port.get_port_groups():
            pg.delete()

    @handle_api_error
    def add_chain_rule(self, chain, action='accept', **kwargs):
        """Create a new accept chain rule."""
        self.mido_api.add_chain_rule(chain, action, **kwargs)
