# Copyright (C) 2017 Nippon Telegraph and Telephone Corporation.
#
# 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 __future__ import absolute_import

import logging
import socket

from sqlalchemy import Column
from sqlalchemy import Boolean
from sqlalchemy import Integer
from sqlalchemy import String

from ryu.lib import ip
from ryu.lib.packet import safi as packet_safi
from ryu.lib.packet import zebra

from . import base
from . import interface


LOG = logging.getLogger(__name__)


class Route(base.Base):
    """
    Route table (like routing table) for Zebra protocol service.

    ``id``: (Primary Key) ID of this route.

    ``family``: Address Family, not AFI (Address Family Identifiers).
    Mostly, "socket.AF_INET" or "socket.AF_INET6".

    ``safi``: Subsequent Address Family Identifiers.

    ``destination``: Destination prefix of this route.

    ``gateway``: Next hop address of this route.
    The default is "" (empty string).

    ``ifindex``: Index of interface to forward packets.

    ``source``: Source IP address of this route, which should be an
     address assigned to the local interface.

    ``route_type``: Route Type of this route.
    This type shows which daemon (or kernel) generated this route.

    ``is_selected``: Whether this route is selected for "destination".
    """
    __tablename__ = 'route'

    id = Column(Integer, primary_key=True)
    family = Column(Integer, default=socket.AF_INET)
    safi = Column(Integer, default=packet_safi.UNICAST)
    destination = Column(String, default='0.0.0.0/0')
    gateway = Column(String, default='')
    ifindex = Column(Integer, default=0)
    source = Column(String, default='')
    route_type = Column(Integer, default=zebra.ZEBRA_ROUTE_KERNEL)
    is_selected = Column(Boolean, default=False)


@base.sql_function
def ip_route_show(session, destination, device, **kwargs):
    """
    Returns a selected route record matching the given filtering rules.

    The arguments are similar to "ip route showdump" command of iproute2.

    :param session: Session instance connecting to database.
    :param destination: Destination prefix.
    :param device: Source device.
    :param kwargs: Filtering rules to query.
    :return: Instance of route record or "None" if failed.
    """
    intf = interface.ip_link_show(session, ifname=device)
    if not intf:
        LOG.debug('Interface "%s" does not exist', device)
        return None

    return session.query(Route).filter_by(
        destination=destination, ifindex=intf.ifindex, **kwargs).first()


@base.sql_function
def ip_route_show_all(session, **kwargs):
    """
    Returns a selected route record matching the given filtering rules.

    The arguments are similar to "ip route showdump" command of iproute2.

    If "is_selected=True", disables the existing selected route for the
    given destination.

    :param session: Session instance connecting to database.
    :param kwargs: Filtering rules to query.
    :return: A list of route records.
    """
    return session.query(Route).filter_by(**kwargs).all()


@base.sql_function
def ip_route_add(session, destination, device=None, gateway='', source='',
                 ifindex=0, route_type=zebra.ZEBRA_ROUTE_KERNEL,
                 is_selected=True):
    """
    Adds a route record into Zebra protocol service database.

    The arguments are similar to "ip route add" command of iproute2.

    If "is_selected=True", disables the existing selected route for the
    given destination.

    :param session: Session instance connecting to database.
    :param destination: Destination prefix.
    :param device: Source device.
    :param gateway: Gateway IP address.
    :param source: Source IP address.
    :param ifindex: Index of source device.
    :param route_type: Route type of daemon (or kernel).
    :param is_selected: If select the given route as "in use" or not.
    :return: Instance of record or "None" if failed.
    """
    if device:
        intf = interface.ip_link_show(session, ifname=device)
        if not intf:
            LOG.debug('Interface "%s" does not exist', device)
            return None
        ifindex = ifindex or intf.ifindex

        route = ip_route_show(session, destination=destination, device=device)
        if route:
            LOG.debug(
                'Route to "%s" already exists on "%s" device',
                destination, device)
            return route

    dest_addr, dest_prefix_num = destination.split('/')
    dest_prefix_num = int(dest_prefix_num)
    if ip.valid_ipv4(dest_addr) and 0 <= dest_prefix_num <= 32:
        family = socket.AF_INET
    elif ip.valid_ipv6(dest_addr) and 0 <= dest_prefix_num <= 128:
        family = socket.AF_INET6
    else:
        LOG.debug('Invalid IP address for "prefix": %s', destination)
        return None
    safi = packet_safi.UNICAST

    if is_selected:
        old_routes = ip_route_show_all(
            session, destination=destination, is_selected=True)
        for old_route in old_routes:
            if old_route:
                LOG.debug('Set existing route to unselected: %s', old_route)
                old_route.is_selected = False

    new_route = Route(
        family=family,
        safi=safi,
        destination=destination,
        gateway=gateway,
        ifindex=ifindex,
        source=source,
        route_type=route_type,
        is_selected=is_selected)

    session.add(new_route)

    return new_route


@base.sql_function
def ip_route_delete(session, destination, **kwargs):
    """
    Deletes route record(s) from Zebra protocol service database.

    The arguments are similar to "ip route delete" command of iproute2.

    :param session: Session instance connecting to database.
    :param destination: Destination prefix.
    :param kwargs: Filtering rules to query.
    :return: Records which are deleted.
    """
    routes = ip_route_show_all(session, destination=destination, **kwargs)
    for route in routes:
        session.delete(route)

    return routes
