"""
Support for Open vSwitch - module with basic Open vSwitch commands.

Suitable for setting up Openstack Neutron.

:codeauthor: Jiri Kotlin <jiri.kotlin@ultimum.io>
"""

import logging

import salt.utils.path
from salt.exceptions import ArgumentValueError, CommandExecutionError
from salt.utils import json

log = logging.getLogger(__name__)


def __virtual__():
    """
    Only load the module if Open vSwitch is installed
    """
    if salt.utils.path.which("ovs-vsctl"):
        return "openvswitch"
    return (False, "Missing dependency: ovs-vsctl")


def _param_may_exist(may_exist):
    """
    Returns --may-exist parameter for Open vSwitch command.

    Args:
        may_exist: Boolean whether to use this parameter.

    Returns:
        String '--may-exist ' or empty string.
    """
    if may_exist:
        return "--may-exist "
    else:
        return ""


def _param_if_exists(if_exists):
    """
    Returns --if-exist parameter for Open vSwitch command.

    Args:
        if_exists: Boolean whether to use this parameter.

    Returns:
        String '--if-exist ' or empty string.
    """
    if if_exists:
        return "--if-exists "
    else:
        return ""


def _retcode_to_bool(retcode):
    """
    Evaulates Open vSwitch command`s retcode value.

    Args:
        retcode: Value of retcode field from response, should be 0, 1 or 2.

    Returns:
        True on 0, else False
    """
    if retcode == 0:
        return True
    else:
        return False


def _stdout_list_split(retcode, stdout="", splitstring="\n"):
    """
    Evaulates Open vSwitch command`s retcode value.

    Args:
        retcode: Value of retcode field from response, should be 0, 1 or 2.
        stdout: Value of stdout filed from response.
        splitstring: String used to split the stdout default new line.

    Returns:
        List or False.
    """
    if retcode == 0:
        ret = stdout.split(splitstring)
        return ret
    else:
        return False


def _convert_json(obj):
    """
    Converts from the JSON output provided by ovs-vsctl into a usable Python
    object tree. In particular, sets and maps are converted from lists to
    actual sets or maps.

    Args:
        obj: Object that shall be recursively converted.

    Returns:
        Converted version of object.
    """
    if isinstance(obj, dict):
        return {_convert_json(key): _convert_json(val) for (key, val) in obj.items()}
    elif isinstance(obj, list) and len(obj) == 2:
        first = obj[0]
        second = obj[1]
        if first == "set" and isinstance(second, list):
            return [_convert_json(elem) for elem in second]
        elif first == "map" and isinstance(second, list):
            for elem in second:
                if not isinstance(elem, list) or len(elem) != 2:
                    return obj
            return {elem[0]: _convert_json(elem[1]) for elem in second}
        else:
            return obj
    elif isinstance(obj, list):
        return [_convert_json(elem) for elem in obj]
    else:
        return obj


def _stdout_parse_json(stdout):
    """
    Parses JSON output from ovs-vsctl and returns the corresponding object
    tree.

    Args:
        stdout: Output that shall be parsed.

    Returns:
        Object represented by the output.
    """
    obj = json.loads(stdout)
    return _convert_json(obj)


def bridge_list():
    """
    Lists all existing real and fake bridges.

    Returns:
        List of bridges (or empty list), False on failure.

    .. versionadded:: 2016.3.0

    CLI Example:

    .. code-block:: bash

        salt '*' openvswitch.bridge_list
    """
    cmd = "ovs-vsctl list-br"
    result = __salt__["cmd.run_all"](cmd)
    retcode = result["retcode"]
    stdout = result["stdout"]
    return _stdout_list_split(retcode, stdout)


def bridge_exists(br):
    """
    Tests whether bridge exists as a real or fake  bridge.

    Returns:
        True if Bridge exists, else False.

    .. versionadded:: 2016.3.0

    CLI Example:

    .. code-block:: bash

        salt '*' openvswitch.bridge_exists br0
    """
    cmd = f"ovs-vsctl br-exists {br}"
    result = __salt__["cmd.run_all"](cmd)
    retcode = result["retcode"]
    return _retcode_to_bool(retcode)


def bridge_create(br, may_exist=True, parent=None, vlan=None):
    """
    Creates a new bridge.

    Args:
        br : string
            bridge name
        may_exist : bool
            if False - attempting to create a bridge that exists returns False.
        parent : string
            name of the parent bridge (if the bridge shall be created as a fake
            bridge). If specified, vlan must also be specified.
        .. versionadded:: 3006.0
        vlan : int
            VLAN ID of the bridge (if the bridge shall be created as a fake
            bridge). If specified, parent must also be specified.
        .. versionadded:: 3006.0

    Returns:
        True on success, else False.

    .. versionadded:: 2016.3.0

    CLI Example:

    .. code-block:: bash

        salt '*' openvswitch.bridge_create br0
    """
    param_may_exist = _param_may_exist(may_exist)
    if parent is not None and vlan is None:
        raise ArgumentValueError("If parent is specified, vlan must also be specified.")
    if vlan is not None and parent is None:
        raise ArgumentValueError("If vlan is specified, parent must also be specified.")
    param_parent = "" if parent is None else f" {parent}"
    param_vlan = "" if vlan is None else f" {vlan}"
    cmd = "ovs-vsctl {1}add-br {0}{2}{3}".format(
        br, param_may_exist, param_parent, param_vlan
    )
    result = __salt__["cmd.run_all"](cmd)
    return _retcode_to_bool(result["retcode"])


def bridge_delete(br, if_exists=True):
    """
    Deletes bridge and all of  its  ports.

    Args:
        br: A string - bridge name
        if_exists: Bool, if False - attempting to delete a bridge that does not exist returns False.

    Returns:
        True on success, else False.

    .. versionadded:: 2016.3.0

    CLI Example:

    .. code-block:: bash

        salt '*' openvswitch.bridge_delete br0
    """
    param_if_exists = _param_if_exists(if_exists)
    cmd = f"ovs-vsctl {param_if_exists}del-br {br}"
    result = __salt__["cmd.run_all"](cmd)
    retcode = result["retcode"]
    return _retcode_to_bool(retcode)


def bridge_to_parent(br):
    """
    .. versionadded:: 3006.0

    Returns the parent bridge of a bridge.

    Args:
        br : string
            bridge name

    Returns:
        Name of the parent bridge. This is the same as the bridge name if the
        bridge is not a fake bridge. If the bridge does not exist, False is
        returned.

    CLI Example:

    .. code-block:: bash

        salt '*' openvswitch.bridge_to_parent br0
    """
    cmd = f"ovs-vsctl br-to-parent {br}"
    result = __salt__["cmd.run_all"](cmd)
    if result["retcode"] != 0:
        return False
    return result["stdout"].strip()


def bridge_to_vlan(br):
    """
    .. versionadded:: 3006.0

    Returns the VLAN ID of a bridge.

    Args:
        br : string
            bridge name

    Returns:
        VLAN ID of the bridge. The VLAN ID is 0 if the bridge is not a fake
        bridge.  If the bridge does not exist, False is returned.

    CLI Example:

    .. code-block:: bash

        salt '*' openvswitch.bridge_to_parent br0
    """
    cmd = f"ovs-vsctl br-to-vlan {br}"
    result = __salt__["cmd.run_all"](cmd)
    if result["retcode"] != 0:
        return False
    return int(result["stdout"])


def port_add(br, port, may_exist=False, internal=False):
    """
    Creates on bridge a new port named port.

    Returns:
        True on success, else False.

    Args:
        br: A string - bridge name
        port: A string - port name
        may_exist: Bool, if False - attempting to create a port that exists returns False.
        internal: A boolean to create an internal interface if one does not exist.

    .. versionadded:: 2016.3.0

    CLI Example:

    .. code-block:: bash

        salt '*' openvswitch.port_add br0 8080
    """
    param_may_exist = _param_may_exist(may_exist)
    cmd = f"ovs-vsctl {param_may_exist}add-port {br} {port}"
    if internal:
        cmd += f" -- set interface {port} type=internal"
    result = __salt__["cmd.run_all"](cmd)
    retcode = result["retcode"]
    return _retcode_to_bool(retcode)


def port_remove(br, port, if_exists=True):
    """
     Deletes port.

    Args:
        br: A string - bridge name (If bridge is None, port is removed from  whatever bridge contains it)
        port: A string - port name.
        if_exists: Bool, if False - attempting to delete a por that  does  not exist returns False. (Default True)

    Returns:
        True on success, else False.

    .. versionadded:: 2016.3.0

    CLI Example:

    .. code-block:: bash

        salt '*' openvswitch.port_remove br0 8080
    """
    param_if_exists = _param_if_exists(if_exists)

    if port and not br:
        cmd = f"ovs-vsctl {param_if_exists}del-port {port}"
    else:
        cmd = f"ovs-vsctl {param_if_exists}del-port {br} {port}"
    result = __salt__["cmd.run_all"](cmd)
    retcode = result["retcode"]
    return _retcode_to_bool(retcode)


def port_list(br):
    """
    Lists all of the ports within bridge.

    Args:
        br: A string - bridge name.

    Returns:
        List of bridges (or empty list), False on failure.

    .. versionadded:: 2016.3.0

    CLI Example:

    .. code-block:: bash

        salt '*' openvswitch.port_list br0
    """
    cmd = f"ovs-vsctl list-ports {br}"
    result = __salt__["cmd.run_all"](cmd)
    retcode = result["retcode"]
    stdout = result["stdout"]
    return _stdout_list_split(retcode, stdout)


def port_get_tag(port):
    """
    Lists tags of the port.

    Args:
        port: A string - port name.

    Returns:
        List of tags (or empty list), False on failure.

    .. versionadded:: 2016.3.0

    CLI Example:

    .. code-block:: bash

        salt '*' openvswitch.port_get_tag tap0
    """
    cmd = f"ovs-vsctl get port {port} tag"
    result = __salt__["cmd.run_all"](cmd)
    retcode = result["retcode"]
    stdout = result["stdout"]
    return _stdout_list_split(retcode, stdout)


def interface_get_options(port):
    """
    Port's interface's optional parameters.

    Args:
        port: A string - port name.

    Returns:
        String containing optional parameters of port's interface, False on failure.

    .. versionadded:: 2016.3.0

    CLI Example:

    .. code-block:: bash

        salt '*' openvswitch.interface_get_options tap0
    """
    cmd = f"ovs-vsctl get interface {port} options"
    result = __salt__["cmd.run_all"](cmd)
    retcode = result["retcode"]
    stdout = result["stdout"]
    return _stdout_list_split(retcode, stdout)


def interface_get_type(port):
    """
    Type of port's interface.

    Args:
        port: A string - port name.

    Returns:
        String - type of interface or empty string, False on failure.

    .. versionadded:: 2016.3.0

    CLI Example:

    .. code-block:: bash

        salt '*' openvswitch.interface_get_type tap0
    """
    cmd = f"ovs-vsctl get interface {port} type"
    result = __salt__["cmd.run_all"](cmd)
    retcode = result["retcode"]
    stdout = result["stdout"]
    return _stdout_list_split(retcode, stdout)


def port_create_vlan(br, port, id, internal=False):
    """
    Isolate VM traffic using VLANs.

    Args:
        br: A string - bridge name.
        port: A string - port name.
        id: An integer in the valid range 0 to 4095 (inclusive), name of VLAN.
        internal: A boolean to create an internal interface if one does not exist.

    Returns:
        True on success, else False.

    .. versionadded:: 2016.3.0

    CLI Example:

    .. code-block:: bash

       salt '*' openvswitch.port_create_vlan br0 tap0 100
    """
    interfaces = __salt__["network.interfaces"]()
    if not 0 <= id <= 4095:
        return False
    elif not bridge_exists(br):
        return False
    elif not internal and port not in interfaces:
        return False
    elif port in port_list(br):
        cmd = f"ovs-vsctl set port {port} tag={id}"
        if internal:
            cmd += f" -- set interface {port} type=internal"
        result = __salt__["cmd.run_all"](cmd)
        return _retcode_to_bool(result["retcode"])
    else:
        cmd = f"ovs-vsctl add-port {br} {port} tag={id}"
        if internal:
            cmd += f" -- set interface {port} type=internal"
        result = __salt__["cmd.run_all"](cmd)
        return _retcode_to_bool(result["retcode"])


def port_create_gre(br, port, id, remote):
    """
    Generic Routing Encapsulation - creates GRE tunnel between endpoints.

    Args:
        br: A string - bridge name.
        port: A string - port name.
        id: An integer - unsigned 32-bit number, tunnel's key.
        remote: A string - remote endpoint's IP address.

    Returns:
        True on success, else False.

    .. versionadded:: 2016.3.0

    CLI Example:

    .. code-block:: bash

       salt '*' openvswitch.port_create_gre br0 gre1 5001 192.168.1.10
    """
    if not 0 <= id < 2**32:
        return False
    elif not __salt__["dig.check_ip"](remote):
        return False
    elif not bridge_exists(br):
        return False
    elif port in port_list(br):
        cmd = "ovs-vsctl set interface {} type=gre options:remote_ip={} options:key={}".format(
            port, remote, id
        )
        result = __salt__["cmd.run_all"](cmd)
        return _retcode_to_bool(result["retcode"])
    else:
        cmd = (
            "ovs-vsctl add-port {0} {1} -- set interface {1} type=gre"
            " options:remote_ip={2} options:key={3}".format(br, port, remote, id)
        )
        result = __salt__["cmd.run_all"](cmd)
        return _retcode_to_bool(result["retcode"])


def port_create_vxlan(br, port, id, remote, dst_port=None):
    """
    Virtual eXtensible Local Area Network - creates VXLAN tunnel between endpoints.

    Args:
        br: A string - bridge name.
        port: A string - port name.
        id: An integer - unsigned 64-bit number, tunnel's key.
        remote: A string - remote endpoint's IP address.
        dst_port: An integer - port to use when creating tunnelport in the switch.

    Returns:
        True on success, else False.

    .. versionadded:: 2016.3.0

    CLI Example:

    .. code-block:: bash

       salt '*' openvswitch.port_create_vxlan br0 vx1 5001 192.168.1.10 8472
    """
    dst_port = " options:dst_port=" + str(dst_port) if 0 < dst_port <= 65535 else ""
    if not 0 <= id < 2**64:
        return False
    elif not __salt__["dig.check_ip"](remote):
        return False
    elif not bridge_exists(br):
        return False
    elif port in port_list(br):
        cmd = (
            "ovs-vsctl set interface {} type=vxlan options:remote_ip={} "
            "options:key={}{}".format(port, remote, id, dst_port)
        )
        result = __salt__["cmd.run_all"](cmd)
        return _retcode_to_bool(result["retcode"])
    else:
        cmd = (
            "ovs-vsctl add-port {0} {1} -- set interface {1} type=vxlan"
            " options:remote_ip={2} options:key={3}{4}".format(
                br, port, remote, id, dst_port
            )
        )
        result = __salt__["cmd.run_all"](cmd)
        return _retcode_to_bool(result["retcode"])


def db_get(table, record, column, if_exists=False):
    """
    .. versionadded:: 3006.0

    Gets a column's value for a specific record.

    Args:
        table : string
            name of the database table
        record : string
            identifier of the record
        column : string
            name of the column
        if_exists : boolean
            if True, it is not an error if the record does not exist.

    Returns:
        The column's value.

    CLI Example:

    .. code-block:: bash

       salt '*' openvswitch.db_get Port br0 vlan_mode
    """
    cmd = ["ovs-vsctl", "--format=json", f"--columns={column}"]
    if if_exists:
        cmd += ["--if-exists"]
    cmd += ["list", table, record]
    result = __salt__["cmd.run_all"](cmd)
    if result["retcode"] != 0:
        raise CommandExecutionError(result["stderr"])
    output = _stdout_parse_json(result["stdout"])
    if output["data"] and output["data"][0]:
        return output["data"][0][0]
    else:
        return None


def db_set(table, record, column, value, if_exists=False):
    """
    .. versionadded:: 3006.0

    Sets a column's value for a specific record.

    Args:
        table : string
            name of the database table
        record : string
            identifier of the record
        column : string
            name of the column
        value : string
            the value to be set
        if_exists : boolean
            if True, it is not an error if the record does not exist.

    Returns:
        None on success and an error message on failure.

    CLI Example:

    .. code-block:: bash

       salt '*' openvswitch.db_set Interface br0 mac 02:03:04:05:06:07
    """
    cmd = ["ovs-vsctl"]
    if if_exists:
        cmd += ["--if-exists"]
    cmd += ["set", table, record, f"{column}={json.dumps(value)}"]
    result = __salt__["cmd.run_all"](cmd)
    if result["retcode"] != 0:
        return result["stderr"]
    else:
        return None
