###############################################################################
# Copyright (c) 2017 Koren Lev (Cisco Systems), Yaron Yogev (Cisco Systems)   #
# and others                                                                  #
#                                                                             #
# All rights reserved. This program and the accompanying materials            #
# are made available under the terms of the Apache License, Version 2.0       #
# which accompanies this distribution, and is available at                    #
# http://www.apache.org/licenses/LICENSE-2.0                                  #
###############################################################################
import json

import re

from discover.fetchers.cli.cli_access import CliAccess
from discover.fetchers.db.db_access import DbAccess
from utils.inventory_mgr import InventoryMgr
from utils.singleton import Singleton


class DbFetchVedgesOvs(DbAccess, CliAccess, metaclass=Singleton):
    def __init__(self):
        super().__init__()
        self.inv = InventoryMgr()
        self.port_re = re.compile("^\s*port (\d+): ([^(]+)( \(internal\))?$")
        self.port_line_header_prefix = " " * 8 + "Port "

    def get(self, id):
        host_id = id[:id.rindex('-')]
        results = self.get_objects_list_for_id(
            """
              SELECT *
              FROM {}.agents
              WHERE host = %s AND agent_type = 'Open vSwitch agent'
            """.format(self.neutron_db),
            "vedge", host_id)
        host = self.inv.get_by_id(self.get_env(), host_id)
        if not host:
            self.log.error("unable to find host in inventory: %s", host_id)
            return []
        host_types = host["host_type"]
        if "Network" not in host_types and "Compute" not in host_types:
            return []
        vsctl_lines = self.run_fetch_lines("ovs-vsctl show", host["id"])
        ports = self.fetch_ports(host, vsctl_lines)
        for doc in results:
            doc["name"] = doc["host"] + "-OVS"
            doc["configurations"] = json.loads(doc["configurations"])
            doc["ports"] = ports
            doc["tunnel_ports"] = self.get_overlay_tunnels(doc, vsctl_lines)
        return results

    def fetch_ports(self, host, vsctl_lines):
        host_types = host["host_type"]
        if "Network" not in host_types and "Compute" not in host_types:
            return {}
        ports = self.fetch_ports_from_dpctl(host["id"])
        self.fetch_port_tags_from_vsctl(vsctl_lines, ports)
        return ports

    def fetch_ports_from_dpctl(self, host_id):
        cmd = "ovs-dpctl show"
        lines = self.run_fetch_lines(cmd, host_id)
        ports = {}
        for l in lines:
            port_matches = self.port_re.match(l)
            if not port_matches:
                continue
            port = {}
            id = port_matches.group(1)
            name = port_matches.group(2)
            is_internal = port_matches.group(3) == " (internal)"
            port["internal"] = is_internal
            port["id"] = id
            port["name"] = name
            ports[name] = port
        return ports

    # from ovs-vsctl, fetch tags of ports
    # example format of ovs-vsctl output for a specific port:
    #        Port "tap9f94d28e-7b"
    #            tag: 5
    #            Interface "tap9f94d28e-7b"
    #                type: internal
    def fetch_port_tags_from_vsctl(self, vsctl_lines, ports):
        port = None
        for l in vsctl_lines:
            if l.startswith(self.port_line_header_prefix):
                port = None
                port_name = l[len(self.port_line_header_prefix):]
                # remove quotes from port name
                if '"' in port_name:
                    port_name = port_name[1:][:-1]
                if port_name in ports:
                    port = ports[port_name]
                continue
            if not port:
                continue
            if l.startswith(" " * 12 + "tag: "):
                port["tag"] = l[l.index(":") + 2:]
                ports[port["name"]] = port
        return ports

    def get_overlay_tunnels(self, doc, vsctl_lines):
        if doc["agent_type"] != "Open vSwitch agent":
            return {}
        if "tunneling_ip" not in doc["configurations"]:
            return {}
        if not doc["configurations"]["tunneling_ip"]:
            self.get_bridge_pnic(doc)
            return {}

        # read the 'br-tun' interface ports
        # this will be used later in the OTEP
        tunnel_bridge_header = " " * 4 + "Bridge br-tun"
        try:
            br_tun_loc = vsctl_lines.index(tunnel_bridge_header)
        except ValueError:
            return []
        lines = vsctl_lines[br_tun_loc + 1:]
        tunnel_ports = {}
        port = None
        for l in lines:
            # if we have only 4 or less spaces in the beginng,
            # the br-tun section ended so return
            if not l.startswith(" " * 5):
                break
            if l.startswith(self.port_line_header_prefix):
                if port:
                    tunnel_ports[port["name"]] = port
                name = l[len(self.port_line_header_prefix):].strip('" ')
                port = {"name": name}
            elif port and l.startswith(" " * 12 + "Interface "):
                interface = l[10 + len("Interface ") + 1:].strip('" ')
                port["interface"] = interface
            elif port and l.startswith(" " * 16):
                colon_pos = l.index(":")
                attr = l[:colon_pos].strip()
                val = l[colon_pos + 2:].strip('" ')
                if attr == "options":
                    opts = val.strip('{}')
                    val = {}
                    for opt in opts.split(", "):
                        opt_name = opt[:opt.index("=")]
                        opt_val = opt[opt.index("=") + 1:].strip('" ')
                        val[opt_name] = opt_val
                port[attr] = val
        if port:
            tunnel_ports[port["name"]] = port
        return tunnel_ports

    def get_bridge_pnic(self, doc):
        conf = doc["configurations"]
        if "bridge_mappings" not in conf or not conf["bridge_mappings"]:
            return
        for v in conf["bridge_mappings"].values(): br = v
        ifaces_list_lines = self.run_fetch_lines("ovs-vsctl list-ifaces " + br,
                                                 doc["host"])
        br_pnic_postfix = br + "--br-"
        interface = ""
        for l in ifaces_list_lines:
            if l.startswith(br_pnic_postfix):
                interface = l[len(br_pnic_postfix):]
                break
        if not interface:
            return
        doc["pnic"] = interface
        # add port ID to pNIC
        pnic = self.inv.find_items({
            "environment": self.get_env(),
            "type": "switch_pnic",
            "host": doc["host"],
            "name": interface
        }, get_single=True)
        if not pnic:
            return
        port = doc["ports"][interface]
        pnic["port_id"] = port["id"]
        self.inv.set(pnic)