#!/usr/bin/python3
"""
generates facts by performing complex magic and parsing, from the output of the actual
remote_facts.sh script, note that it also makes api calls to the remote node, and
accepts a single argument - the node's lhvpn ip address
"""

import base64
import json
import os
import sys
from typing import Dict, List
from central_dop.api.dop import validate_config
from central_dop import interface, nodes
from netops.ngcs import client, models


def main():
    # parse the raw input data from cli args
    data = {}
    for arg in [
                   'system_interfaces',
                   'system_macs',
                   'ethtool_interfaces',
                   'iflinks'
               ] + sys.stdin.read().split():
        key, value = split_key_value(arg)
        data[key] = globals()['parse_' + key](value)
    if os.getenv('DEBUG') == 'true':
        print(json.dumps(data), file=sys.stderr)

    api_certs = nodes.get_certs()

    # fetch additional data from the api
    ngcs = client.Api(
        address=sys.argv[1],
        cert=api_certs,
        version='2.2',
        verify=False,
    )
    firewall_zones = ngcs.get_firewall_zones()
    interfaces = ngcs.get_interfaces()
    connections = ngcs.get_connections()
    failover_cellular = ngcs.get_failover_cellular()

    # map ngcs /physifs ids for all system interfaces (might be None if not found)
    system_interface_ids = {}
    for system_interface in data['system_interfaces']:
        system_interface_id = None
        for iface in interfaces:
            if iface.device != system_interface:
                continue
            system_interface_id = iface.id
            break
        system_interface_ids[system_interface] = system_interface_id

    # decide the interface (and connection)

    def system_interfaces() -> List[str]:
        return data['system_interfaces']

    def system_mac_address(system_interface: str) -> str:
        if system_interface not in data['system_macs']:
            raise interface.UndecidableException('no mac data for interface {}'.format(system_interface))
        return data['system_macs'][system_interface]

    def system_failover_cellular() -> models.FailoverToCellular:
        return failover_cellular

    def ethtool_stdout(system_interface: str) -> str:
        if system_interface not in data['ethtool_interfaces']:
            raise interface.UndecidableException('no ethtool data for interface {}'.format(system_interface))
        return data['ethtool_interfaces'][system_interface]

    def interface_link(system_interface: str) -> str:
        """Takes an interface name and returns the name of its linked interface, if any.
        Example: sw0p1 should return sw0
        """
        if system_interface not in data['iflinks']:
            raise interface.UndecidableException(
                'no iflink data for interface {}'.format(system_interface))

        # Format is '<ifindex> <iflink>'
        interface_link = data['iflinks'][system_interface][1]
        for interface_name, iflink_data in data['iflinks'].items():
            if iflink_data[0] == interface_link:
                return interface_name

        return None

    def api_firewall_zone(system_interface: str) -> models.FirewallZone:
        for firewall_zone in firewall_zones:
            if system_interface_ids[system_interface] and system_interface_ids[system_interface] in firewall_zone.interfaces:
                return firewall_zone
        raise interface.UndecidableException('no firewall zone data for interface {}'.format(system_interface))

    def api_interface(system_interface: str) -> models.Interface:
        for iface in interfaces:
            if system_interface_ids[system_interface] and iface.id == system_interface_ids[system_interface]:
                return iface
        raise interface.UndecidableException('no interface data for interface {}'.format(system_interface))

    def api_interface_by_id(interface_id: str) -> models.Interface:
        for iface in interfaces:
            if iface.id == interface_id:
                return iface
        raise interface.UndecidableException(
            'no interface found for ID {}'.format(interface_id))

    def api_connections(system_interface: str) -> List[models.Connection]:
        result = []
        for connection in connections:
            if system_interface_ids[system_interface] and connection.interface == system_interface_ids[system_interface]:
                result.append(connection)
        return result

    def configure_connection(conn, intf):
        """
            Add/Update the device connection
        """
        # Add a new connection on the interface
        global_address = sys.argv[2]
        global_netmask = sys.argv[3]

        errmsg = validate_config(global_address, global_netmask)
        if errmsg:
            raise interface.UndecidableException("Global config values: ipaddress {}, subnet_mask {} are not valid, {}".format
                                                        (global_address, global_netmask, errmsg))
        connection = models.Connection(
        {
            "physif": intf.id,
            "mode": 'static',
            "ipv4_static_settings": {
                "netmask": global_netmask,
                "address": global_address
            }
        },
        )
        ngcs.post_connection(connection)

        return connection

    decider = interface.Decider(
        system_interfaces=system_interfaces,
        system_mac_address=system_mac_address,
        ethtool_stdout=ethtool_stdout,
        api_firewall_zone=api_firewall_zone,
        api_interface=api_interface,
        api_interface_by_id=api_interface_by_id,
        interface_link=interface_link,
        api_connections=api_connections,
        system_failover_cellular=system_failover_cellular,
    )

    decider.decide()

    # verify if the decided interface has IP configured.
    connection = decider.connection
    if not connection:
        connection = configure_connection(decider.connection, decider.interface)

    print(json.dumps({
        'nom_remote_interface': decider.interface.device,
        'nom_remote_mac': decider.mac_address,
        'nom_remote_server': connection.ipv4_static_settings.address,
        'nom_remote_netmask': connection.ipv4_static_settings.netmask,
        'nom_remote_netmask_cidr': connection.ipv4_static_settings.netmask_cidr,
    }))



def split_key_value(arg):
    """
    splits arg which is key=base64_value, returns (key, value) where value may be either None or bytes
    :param arg: should be a string
    :return:
    """
    key = arg
    value = None
    index = arg.find('=')
    if index != -1:
        key = arg[:index]
        value = base64.b64decode(arg[index + 1:])
    return key, value


def parse_system_interfaces(value) -> List[str]:
    """
    returns list of strings like net1, wlan, etc
    """
    if not value:
        return []
    return value.decode('utf-8').split()


def parse_system_macs(value) -> Dict[str, str]:
    """
    returns dictionary of any system_interfaces that we were able to retrieve the mac addresses for
    """
    if not value:
        return {}
    result = {}
    for line in value.decode('utf-8').splitlines():
        key, value = split_key_value(line)
        value = value.decode('utf-8').strip()
        if value != '':
            result[key] = value
    return result


def parse_ethtool_interfaces(value) -> Dict[str, str]:
    """
    returns dictionary of any system_interfaces that we were able to run `ethtool <interface>` (values are stdout of that command)
    """
    if not value:
        return {}
    result = {}
    for line in value.decode('utf-8').splitlines():
        key, value = split_key_value(line)
        result[key] = value.decode('utf-8')
    return result


def parse_iflinks(value) -> Dict[str, str]:
    if not value:
        return {}
    result = {}
    for line in value.decode('utf-8').splitlines():
        key, value = split_key_value(line)
        result[key] = value.decode('utf-8').strip().split(",")

    return result

if __name__ == "__main__":
    main()
