#!/usr/libexec/platform-python

import argparse
import ipaddress
import json
import logging
import pprint
import re
import sys
from pathlib import Path

from cloudinit import stages
try:
    from cloudinit.log import loggers as ci_loggers
except:
    # cloud-init pre 71cc75ceed87676ee8efc4feb10de3238d758afe
    # likely < 24.4
    from cloudinit import log as ci_loggers
from cloudinit.net.network_state import parse_net_config_data
from cloudinit.net.renderers import NAME_TO_RENDERER
from cloudinit.sources.DataSourceScaleway import DataSourceScaleway

ADDRESSES_CFG_FIELD = "addresses"

log_fmt = logging.Formatter("[%(asctime)s] %(levelname)s: %(message)s")
log_hdr = logging.StreamHandler()
log_hdr.setFormatter(log_fmt)

logger = logging.getLogger(__name__)
logger.propagate = False
logger.addHandler(log_hdr)
logger.setLevel(logging.INFO)

def mac2eui64(mac, prefix=None):
    """
    Convert a MAC address to a EUI64 address
    or, with prefix provided, a full IPv6 address
    From: https://gist.github.com/wido/f5e32576bb57b5cc6f934e177a37a0d3
    """
    # http://tools.ietf.org/html/rfc4291#section-2.5.1
    eui64 = re.sub(r"[.:-]", "", mac).lower()
    eui64 = eui64[0:6] + "fffe" + eui64[6:]
    eui64 = hex(int(eui64[0:2], 16) ^ 2)[2:].zfill(2) + eui64[2:]

    if prefix is None:
        return ":".join(re.findall(r".{4}", eui64))

    net = ipaddress.ip_network(prefix, strict=False)
    euil = int("0x{0}".format(eui64), 16)
    return str(net[euil])

def var_dump(varname):
    data = pprint.pformat(globals()[varname])
    title = f" {varname} ".center(60, "-")
    return f"{title}\n{data}"

def parse_args():
    parser = argparse.ArgumentParser()
    parser.add_argument("--verbose", "-v", action="store_true", help="be verbose")
    parser.add_argument("--render", action="store_true", help="render configuration")
    parser.add_argument("--apply", action="store_true", help="apply configuration")
    parser.add_argument("datafile", type=Path, help="network details JSON file")
    return parser, parser.parse_args()

if __name__ == "__main__":
    parser, args = parse_args()

    with open(args.datafile) as f:
        json_data = json.load(f)

    if not json_data:
        print("No network configuration data. Not changing anything.")
        sys.exit(0)

    if args.verbose:
        logger.setLevel(logging.DEBUG)

    # Initialize basic cloud-init objects and logging
    logger.info("Inititating network reconfiguration")
    init = stages.Init()
    init.read_cfg()
    try:
        ci_loggers.setup_logging(init.cfg)
    except AttributeError:
        # cloud-init pre c1f6f59e9fc354b5e7b459c23044bc2177bd1d87
        # likely < 23.4
        ci_loggers.setupLogging(init.cfg)
    logger.disabled = False

    # distro handles the characteristics of the OS at hand
    distro = init.distro

    # Grab references to the network configuration renderer/activators suited for the
    # present OS
    try:
        renderer = distro.network_renderer
    except AttributeError:
        # cloud-init pre 825eb1e7b8d558c896478deceef2c3a08a34a010
        # likely < 24.2
        renderer = distro._get_renderer()
    activator = distro.network_activator

    # Use the Scaleway datasource
    ds = DataSourceScaleway({}, distro, init.paths)

    ####################################################################################
    # Save the main interface name for later use
    #
    # `fallback_interface` used to be a property of the DataSource class. It has been
    # moved to the Distro class (embedded in the DataSource object through the `distro`
    # attribute) as of the version 24.1 of cloud-init.
    # See: https://github.com/canonical/cloud-init/commit/21b2b6e4423b0fec325b32042c05ef4274cdd301
    ####################################################################################
    try:
        main_netif = ds.distro.fallback_interface
    except AttributeError:
        # cloud-init < 24.1
        main_netif = ds.fallback_interface
    logger.debug(var_dump("main_netif"))

    # Grab the MAC address of the main interface
    # Useful to compute EUI-64 for IPv6
    main_mac = distro.networking.get_interface_mac(main_netif)
    logger.debug(var_dump("main_mac"))

    # Inject local JSON data into the DS (simulating a ds.get_data() call)
    ds.metadata = json_data
    logger.debug(var_dump("json_data"))

    # Special treatment for corner cases like no address, or IPv6-only
    if not any([ds.metadata["public_ips_v4"], ds.metadata["public_ips_v6"]]):
        logger.error("Not implemented: No address. Aborting")
        sys.exit(1)
    elif not ds.metadata["public_ips_v4"]:
        logger.error("Not implemented: IPv6-only. Aborting")
        sys.exit(1)

    # Because we do not call ds.get_data(), filter the IPv4 address w/
    # provisioning_mode=dhcp and assign it to ds.ephemeral_fixed_address for the next
    # steps to work correctly.
    # See: https://github.com/canonical/cloud-init/blob/9d598f238d2b3a5f348c90ce870aa06177a9f93d/cloudinit/sources/DataSourceScaleway.py#L367
    #      https://github.com/canonical/cloud-init/blob/9d598f238d2b3a5f348c90ce870aa06177a9f93d/cloudinit/sources/DataSourceScaleway.py#L308
    dhcp_addresses = [
        x for x in ds.metadata["public_ips_v4"]
        if x["provisioning_mode"] == "dhcp"
    ]
    ds.ephemeral_fixed_address = dhcp_addresses[0]["address"]

    # Extract the network configuration as parsed by the datasource
    netcfg = ds.network_config
    logger.debug(var_dump("netcfg"))
    iface_config_1 = iface_config = netcfg["ethernets"][main_netif]
    logger.debug(var_dump("iface_config_1"))

    # Make the addresses list mutable
    if ADDRESSES_CFG_FIELD in iface_config:
        iface_config[ADDRESSES_CFG_FIELD] = list(iface_config[ADDRESSES_CFG_FIELD])
    else:
        iface_config[ADDRESSES_CFG_FIELD] = []

    # Handle non-SLAAC IPv6
    for ip in ds.metadata["public_ips_v6"]:
        if ip["provisioning_mode"] != "manual":
            continue
        net6 = f"{ip['address']}/{ip['netmask']}"
        cidr6 = mac2eui64(main_mac, prefix=net6)
        logger.debug(var_dump("cidr6"))
        iface_config[ADDRESSES_CFG_FIELD].append(f"{cidr6}/128")

    # Revert the addresses list to be immutable
    iface_config[ADDRESSES_CFG_FIELD] = tuple(iface_config[ADDRESSES_CFG_FIELD])
    iface_config_2 = iface_config
    logger.debug(var_dump("iface_config_2"))

    # Transform the datasource's network configuration into a structure expected by the
    # renderer
    nstate = parse_net_config_data(netcfg)
    nstate_network_state_1 = nstate._network_state
    logger.debug(var_dump("nstate_network_state_1"))

    # Hack for the NetworkManager renderer to get multi-IPv6 working.
    # The goal is to force ipv6.method to auto instead of manual on the NM connection.
    # ipv6_slaac wrongly seems to be a good candidate, but it disables IPv4. Using dhcp6
    # instead.
    if NAME_TO_RENDERER["network-manager"].__name__ == renderer.__module__:
        subnets = nstate._network_state["interfaces"][main_netif]["subnets"]
        for subnet in subnets:
            if subnet.get("prefix") == 128:
                subnet["type"] = "dhcp6"

    nstate_network_state_2 = nstate._network_state
    nstate_config = nstate.config
    logger.debug(var_dump("nstate_network_state_2"))
    logger.debug(var_dump("nstate_config"))

    # Render the network configuration files on disk
    if args.render:
        logger.info("Rendering new network configuration files")
        renderer.render_network_state(nstate)
        nstate_network_state_3 = nstate._network_state
        logger.debug(var_dump("nstate_network_state_3"))
    else:
        logger.warning("NOT rendering new network configuration files")

    # Apply the configuration according to what has been rendered
    if args.apply:
        logger.info("Activating new network configuration")
        activator.bring_up_all_interfaces(nstate)
    else:
        logger.warning("NOT activating new network configuration")
