#!/usr/bin/python3

#
# Copyright (C) 2022 Nethesis S.r.l.
# SPDX-License-Identifier: GPL-2.0-only
#

import random
import hashlib
import ipaddress
import nsmigration
from nethsec import firewall, utils
from euci import UciExceptionNotFound

(u, data, nmap) = nsmigration.init("network.json")
vlans = dict()
skipped_vlans = list()
alias_zones = dict()
devices = dict()
bond_zones = dict()
bonds = dict()
interface_names = dict()

def exists(key):
    try:
        u.get('network', key)
        return True
    except UciExceptionNotFound:
        return False

def get_alias_by_device(u, device):
    if not device or device == '@None':
        return None

    for i in utils.get_all_by_type(u, 'network', 'interface'):
        if u.get('network', i, 'device', default=None) == device:
            return i

    return None

def create_vlan(v):
    if 'hwaddr' in v:
        iname = utils.get_device_name(nsmigration.remap(v["hwaddr"], nmap))
    else:
        if v['device'].startswith('bond'):
            # uci adds 'bond-' prefix to bonding interfaces
            iname = f"bond-{v['device']}"
        else:
            iname = v['device']
        v['hwaddr'] = iname # for later use
    if iname is None:
        nsmigration.vprint(f'Skipping vlan {v["vid"]} on {v["hwaddr"]}')
        return
    vname = utils.sanitize(f'vlan{v["vid"]}')
    # avoid vlan override if multiple vlan has the same vid
    if exists(vname):
        digest = hashlib.md5(b'{v["hwaddr"]}{v["vid"]}').hexdigest()[:5]
        vname = utils.sanitize(f'vlan{v["vid"]}{digest}')
    nsmigration.vprint(f"Creating vlan for {iname}.{v['vid']}")
    u.set("network", vname, "device") # create named record
    u.set("network", vname, "type", v["type"])
    u.set("network", vname, "vid", v["vid"])
    u.set("network", vname, "ifname", iname)
    u.set("network", vname, "name", f'{iname}.{v["vid"]}')
    # save vlan map for later user
    vlans[f'{v["hwaddr"]}.{v["vid"]}'] = f'{iname}.{v["vid"]}'


# Cleanup default firewall zones
for section in u.get("firewall"):
    s_type = u.get("firewall", section)
    if s_type == "zone":
        zname = u.get("firewall", section, "name")
        if zname == "wan" or zname == "wan6" or zname == "lan":
            nsmigration.vprint(f"Deleting zone {section} ({zname})")
            u.delete("firewall", section)

# Cleanup default network configuration
for section in u.get("network"):
    s_type = u.get("network", section)
    if s_type == "interface" and (section == "wan" or section == "wan6" or section == "lan"):
        nsmigration.vprint(f"Deleting interface {section}")
        u.delete("network", section)
    if s_type == "device":
        ts = u.get_all("network", section)
        if ts['name'] == 'br-lan':
            nsmigration.vprint(f"Deleting interface {section}")
            u.delete("network", section)

# Cleanup default qosify interfaces and devices
for section in u.get("qosify"):
    if u.get("qosify", section) == "device" or u.get("qosify", section) == "interface":
        u.delete("qosify", section)

# Create vlan devices
for v in data['vlans']:
    if not 'hwaddr' in v:
        # vlan over logical interface, just postpone it
        skipped_vlans.append(v)
        continue

    devices[nsmigration.remap(v["hwaddr"], nmap)] = 1
    create_vlan(v)

# Create bonds: 3 records required
# - bond interface with management ip address
# - bond device
# - real interface attached to bond
for b in data['bonds']:
    # create bond interface
    bname = utils.sanitize(b["name"])
    nsmigration.vprint(f"Creating bond {b['name']}")
    u.set("network", bname, "interface") # create named record
    u.set("network", bname, "proto", "bonding")
    u.set("network", bname, "bonding_policy", b["bonding_policy"])
    slaves = list()
    for s in b["slaves"]:
        slave = utils.get_device_name(nsmigration.remap(s, nmap))
        if slave:
            slaves.append(utils.get_device_name(nsmigration.remap(s, nmap)))
    u.set("network", bname, "slaves", slaves)
    # setup mii monitoring as NS7
    if b["bonding_policy"] != "802.3ad":
        u.set("network", bname, "link_monitoring", "mii")
        u.set("network", bname, "miimon", "100")
    u.set("network", bname, "netmask", "255.255.255.255")
    u.set("network", bname, "ipaddr", f"127.{random.randint(1, 254)}.{random.randint(1, 254)}.1")

    # create interface attached to bond
    iname = utils.sanitize(f'{b["name"]}_{b["zone"]}')
    bonds[b["name"]] = iname
    if not b["zone"] in bond_zones:
        bond_zones[b["zone"]] = list()
    bond_zones[b["zone"]].append(iname) # attach to the zone later
    u.set("network", iname, "interface") # create named record
    u.set("network", iname, "device", f'bond-{bname}')
    if 'ipaddr' in b and str(b['ipaddr']) != "":
        u.set("network", iname, 'ipaddr', b['ipaddr'])
        u.set("network", iname, 'proto', 'static')
        for opt in ('netmask', 'gateway'):
            if opt in b and str(b[opt]) != "":
                u.set("network", iname, opt, b[opt])
    else:
        u.set("network", iname, 'proto', 'dhcp')

    # create bond device
    bid = utils.get_random_id()
    u.set("network", bid, "device")
    u.set("network", bid, "name", f"bond-{bname}")
    u.set("network", bid, "ipv6", "0")

# Create bridges
for b in data['bridges']:
    bname = utils.sanitize(b["name"])
    nsmigration.vprint(f"Creating bridge {b['name']}")
    u.set("network", bname, "device") # create named record
    u.set("network", bname, 'name', b['name'])
    u.set("network", bname, "type", "bridge")
    u.set("network", bname, "ipv6", "0")
    ports = list()
    for p in b["ports"]:
        if p['type'] == 'ethernet':
            bdevice = utils.get_device_name(nsmigration.remap(p['hwaddr'], nmap))
            devices[nsmigration.remap(p["hwaddr"], nmap)] = 1
        elif p['type'] == 'vlan':
            bdevice = vlans[f"{p['hwaddr']}.{p['vid']}"]
            devices[nsmigration.remap(p["hwaddr"], nmap)] = 1
        else:
            # virtual device like bonds
            bdevice = p['device']
        if bdevice:
            ports.append(bdevice)
    u.set("network", bname, 'ports', ports)

# Re-create skipped vlans
for v in skipped_vlans:
    create_vlan(v)

# Create aliases
acount = len(data['aliases'])
for a in data['aliases']:
    device = ''
    aname = ''
    if 'hwaddr' in a:
        # alias over vlan
        if 'vid' in a:
            vlan = vlans.get(f'{a["hwaddr"]}.{a["vid"]}', None)
            if vlan is None:
                nsmigration.vprint(f'Skipping alias for non-exiting vlan {a["vid"]} on {a["hwaddr"]}')
                continue
            aname = "al_" + utils.sanitize(vlan)+str(acount)
            i = utils.get_interface_from_device(u, vlan)
            if i:
                device = f'@{i}'
            else:
                device = f'FIXME|{vlan}'
        # alias over ethernet interface
        else:
            # normal interfaces uses @ prefix with interface name
            # the name will be resolved at the end
            device = utils.get_device_name(nsmigration.remap(a['hwaddr'], nmap))
            if not device:
                print(f"Skipping alias for device {device}")
                continue
            aname = "al_" + utils.sanitize(device)+"_"+str(acount)
            device = f'FIXME|{device}'

        devices[nsmigration.remap(a["hwaddr"], nmap)] = 1
    # alias over logical interface
    else:
        aname = "al_" + utils.sanitize(a["device"])+str(acount)
        # add @ prefix for bridges and bonds
        device = f'FIXME|{a["device"]}'
    if not device:
        nsmigration.vprint(f'Skipping alias {aname}')
        continue

    ipaddr = ipaddress.IPv4Interface(f'{a["ipaddr"]}/{a["netmask"]}')
    alias_id = get_alias_by_device(u, device)
    if alias_id:
        nsmigration.vprint(f'Updating alias {aname}')
        cur_ips = list(u.get_all("network", alias_id, "ipaddr"))
        cur_ips.append(ipaddr.with_prefixlen)
        u.set("network", alias_id, "ipaddr", cur_ips)
    else:
        nsmigration.vprint(f'Creating alias {aname}')
        u.set("network", aname, "interface") # create named record
        u.set("network", aname, "device", device)
        u.set("network", aname, "ipaddr", [ipaddr.with_prefixlen])
        u.set("network", aname, "proto", a["proto"])
        if a.get("gateway"):
            u.set("network", aname, "gateway", a["gateway"])
        acount = acount - 1
        if not a['zone'] in alias_zones:
            alias_zones[a['zone']] = list()
        alias_zones[a['zone']].append(aname)

# Create interfaces
for i in data['interfaces']:
    siname = utils.sanitize(i["interface"])
    if i["proto"] == "pppoe":
        iname = siname[0:8] # make sure interface name is 8 chars max, reserve space for "pppoe-" prefix
    else:
        iname = siname[0:14] # make sure interface name is 15 chars max
    interface_names[siname] = iname
    nsmigration.vprint(f'Creating interface {iname}')
    u.set("network", iname, "interface") # create named record
    u.set("network", iname, "proto", i["proto"])
    for opt in ('ipaddr', 'netmask', 'gateway', 'ipv6', 'username', 'password', 'keepalive'):
        if opt in i and str(i[opt]) != "":
            u.set("network", iname, opt, i[opt])
    # virtual interface
    if not 'hwaddr' in i:
        if 'vid' in i:
            if i['device'].startswith('bond'):
                # uci adds 'bond-' prefix to bonding interfaces
                i['device'] = f"bond-{i['device']}"
            u.set("network", iname, "device", f'{i["device"]}.{i["vid"]}')
        else:
            u.set("network", iname, "device", i["device"])
    else:
        devices[nsmigration.remap(i["hwaddr"], nmap)] = 1
        ndevice = utils.get_device_name(nsmigration.remap(i['hwaddr'], nmap))
        if 'vid' in i:
            u.set("network", iname, "device", f'{ndevice}.{i["vid"]}')
        else:
            u.set("network", iname, "device", ndevice)

    if i["zone"] == "wan":
        if i["bandwidth_up"] and i["bandwidth_down"]:
            bandwidth_up = int(float(i["bandwidth_up"].removesuffix("kbit")) / 1000)
            bandwidth_down = int(float(i["bandwidth_down"].removesuffix("kbit")) / 1000)
            u.set("qosify", iname, "interface")
            u.set("qosify", iname, "name", iname)
            u.set("qosify", iname, "disabled", "0")
            u.set("qosify", iname, "bandwidth_down", f'{bandwidth_down}mbit')
            u.set("qosify", iname, "bandwidth_up", f'{bandwidth_up}mbit')
            u.set("qosify", iname, "overhead_type", "none")

# Create firewall zones
for z in data['zones']:
    nsmigration.vprint(f'Creating zone {z["name"]}')
    zname = utils.get_id(z["name"])
    u.set("firewall", zname, "zone") # create named record
    u.set("firewall", zname, "name", z["name"])
    u.set("firewall", zname, "output", z["output"])
    u.set("firewall", zname, "input", z["input"])
    u.set("firewall", zname, "forward", z["forward"])
    if z["name"] in bond_zones:
        for b in bond_zones[z["name"]]:
            base_name = b.split("_")[0]
            if base_name in z["network"]:
                z["network"].remove(base_name) # avoid duplicate bond like bond0 and bond0_lan
        z["network"] = z["network"] + bond_zones[z["name"]]
    # retrieve reduced network name, if present
    u.set("firewall", zname, "network", [interface_names.get(utils.sanitize(n), utils.sanitize(n)) for n in z["network"]])
    if z["name"].startswith("wan"): # setup masquerading for wans
        u.set("firewall", zname, "masq", '1')
        u.set("firewall", zname, "mtu_fix", '1')
    if z["name"].startswith("guest"): # setup DNS and DHCP access for guests
        firewall.add_template_rule(u, 'ns_guest_dns', tags=[])
        firewall.add_template_rule(u, 'ns_guest_dhcp', tags=[])

# Setup firewall zones for aliases
for z in alias_zones:
    zname = utils.get_id(z)
    network = list(u.get("firewall", zname, 'network', default=list(), list=True))
    network = network + alias_zones[z]
    u.set("firewall", zname, "network", [utils.sanitize(n) for n in network])

# Create firewall forwardings
for f in data['forwardings']:
    fname = utils.get_id(f'{f["src"]}2{f["dest"]}')
    nsmigration.vprint(f'Creating forwarding {fname}')
    u.set("firewall", fname, "forwarding") # create named record
    u.set("firewall", fname, "src", f["src"])
    u.set("firewall", fname, "dest", f["dest"])

# Create snat rules
for s in data['snats']:
    nsmigration.vprint(f'Creating SNAT {s["name"]}')
    sname = utils.get_id(s["name"])
    u.set("firewall", sname, "nat") # create named record
    u.set("firewall", sname, "name", s["name"])
    u.set("firewall", sname, "target", s["target"])
    u.set("firewall", sname, "proto", [s["proto"]])
    u.set("firewall", sname, "snat_ip", s["snat_ip"])
    u.set("firewall", sname, "src_ip", s["src_ip"])
    u.set("firewall", sname, "dest_ip", s.get("dest_ip", "0.0.0.0/0"))
    u.set("firewall", sname, "src", "wan")

# Loop through aliases and fix dangling references to interface
# This should be done as last thing, because when creating aliases
# the interfaces are still not created
interfaces = utils.get_all_by_type(u, 'network', 'interface')
for i in interfaces:
    device = interfaces[i].get('device', '')
    if device.startswith('FIXME|'):
        device = device.split('|')[1]
        # map the device to the interface
        # if the interface is a bond, resolve the interface with the zone not the fake bond interface
        if device.startswith('bond'):
            interface = bonds[device]
        else:
            interface = utils.get_interface_from_device(u, device)
        if interface:
            nsmigration.vprint(f'Mapping alias {device} to {interface}')
            u.set("network", i, 'device', f'@{interface}')
        else:
            # fallback to the name the device: the configuration works but the UI will probably fail
            u.set("network", i, 'device', f'@{device}')

# Avoid duplicate devices
existing_devices = []
for d in utils.get_all_by_type(u, 'network', 'device'):
    device = u.get('network', d, 'name', default=None)
    if device:
        existing_devices.append(device)

# Create all physical devices: this is not required by UCI but for the UI
for d in devices.keys():
    if utils.get_device_name(d) in existing_devices:
        continue
    did = utils.get_random_id()
    u.set("network", did, "device")
    u.set("network", did, "name", utils.get_device_name(d))
    u.set("network", did, "ipv6", "0")

# Save configuration
u.commit("network")
u.commit("firewall")
u.commit("qosify")
