# import logging

# logging.basicConfig()
# logging.getLogger().setLevel(logging.DEBUG)

import asyncio
import os
import subprocess
from collections.abc import Sequence
from typing import Callable

from flux_networking_shared.models.network import NetworkInterface

# just use all the subiquity / probert models
from probert.network import Link, NetworkEventReceiver, UdevObserver

# Standard interface flags (net/if.h)
IFF_UP = 0x1  # Interface is up.
IFF_BROADCAST = 0x2  # Broadcast address valid.
IFF_DEBUG = 0x4  # Turn on debugging.
IFF_LOOPBACK = 0x8  # Is a loopback net.
IFF_POINTOPOINT = 0x10  # Interface is point-to-point link.
IFF_NOTRAILERS = 0x20  # Avoid use of trailers.
IFF_RUNNING = 0x40  # Resources allocated.
IFF_NOARP = 0x80  # No address resolution protocol.
IFF_PROMISC = 0x100  # Receive all packets.
IFF_ALLMULTI = 0x200  # Receive all multicast packets.
IFF_MASTER = 0x400  # Master of a load balancer.
IFF_SLAVE = 0x800  # Slave of a load balancer.
IFF_MULTICAST = 0x1000  # Supports multicast.
IFF_PORTSEL = 0x2000  # Can set media type.
IFF_AUTOMEDIA = 0x4000  # Auto media select active.

IFA_F_PERMANENT = 0x80

NETDEV_IGNORED_IFACE_TYPES = [
    "lo",
    "bridge",
    "tun",
    "tap",
    "dummy",
    "sit",
    "can",
    "???",
]


def _clean_env(env, *, locale=True):
    if env is None:
        env = os.environ.copy()
    else:
        env = env.copy()
    if locale:
        env["LC_ALL"] = "C"

    return env


def run_command(
    cmd: Sequence[str],
    *,
    input=None,
    stdout=subprocess.PIPE,
    stderr=subprocess.PIPE,
    encoding="utf-8",
    errors="replace",
    env=None,
    clean_locale=True,
    **kw,
) -> subprocess.CompletedProcess:
    """A wrapper around subprocess.run with logging and different defaults.

    We never ever want a subprocess to inherit our file descriptors!
    """
    if input is None:
        kw["stdin"] = subprocess.DEVNULL
    else:
        input = input.encode(encoding)
    print(f"run_command called: {cmd}")
    try:
        cp: subprocess.CompletedProcess = subprocess.run(
            cmd,
            input=input,
            stdout=stdout,
            stderr=stderr,
            env=_clean_env(env, locale=clean_locale),
            **kw,
        )
        if encoding:
            if isinstance(cp.stdout, bytes):
                cp.stdout = cp.stdout.decode(encoding)
            if isinstance(cp.stderr, bytes):
                cp.stderr = cp.stderr.decode(encoding)
    except subprocess.CalledProcessError as e:
        print(f"run_command: {e}")
        raise
    else:
        print(f"run_command: {cp.args} exited with code: {cp.returncode}")
        return cp


class Receiver(NetworkEventReceiver):
    def __init__(self, callback: Callable[[NetworkInterface], None]):
        self.all_links: dict[int, Link] = {}
        self.callback = callback
        # self.route_data = []

    def new_link(self, ifindex: int, link: Link):
        if link.type in NETDEV_IGNORED_IFACE_TYPES:
            return

        self.all_links[ifindex] = link

        # we assume all new links are down
        # is_connected = False

        flags = getattr(link, "flags", 0)
        is_connected = bool(flags & IFF_UP)

        print(f"Observer new link: {is_connected} for link: {link.name}")

        # I was doing this only on update link, but if the link is actually down
        # you don't get the update
        self.callback(
            NetworkInterface.from_probe_data(link.serialize(), is_connected),
        )

    def route_change(self, action: str, data):
        # self.route_data.append(data)
        pass

    def update_link(self, ifindex: int):
        link = self.all_links.get(ifindex)

        if not link:
            return

        flags = getattr(link, "flags", 0)
        is_connected = bool(flags & IFF_UP)

        print(f"Observer updated link: {is_connected} for link: {link.name}")

        self.callback(
            NetworkInterface.from_probe_data(link.serialize(), is_connected),
        )

    def del_link(self, ifindex):
        try:
            del self.all_links[ifindex]
        except KeyError:
            pass


class Observer:
    def __init__(self, receiver: Receiver) -> None:
        self.receiver: Receiver | None = receiver
        self.observer: UdevObserver | None = UdevObserver(self.receiver)
        self._observer_fds: list[int] = []

        self._watching = False

    def start(self) -> None:
        self._observer_fds.extend(self.observer.start())
        self.start_watching()

    def stop(self) -> None:
        self.stop_watching()

        self._observer_fds = []
        self.observer = None
        self.receiver = None

    def stop_watching(self):
        if not self._watching:
            return

        loop = asyncio.get_running_loop()

        for fd in self._observer_fds:
            loop.remove_reader(fd)

        self._watching = False

    def start_watching(self):
        if self._watching:
            return

        self._watching = True

        loop = asyncio.get_running_loop()

        for fd in self._observer_fds:
            loop.add_reader(fd, self._data_ready, fd)

    def _data_ready(self, fd):
        cp = run_command(["udevadm", "settle", "-t", "0"])
        if cp.returncode != 0:
            print("waiting 0.1 to let udev event queue settle")
            self.stop_watching()
            loop = asyncio.get_running_loop()
            loop.call_later(0.1, self.start_watching)
            return
        self.observer.data_ready(fd)


if __name__ == "__main__":

    async def main():
        obs = Observer()
        obs.start()

    loop = asyncio.new_event_loop()
    loop.create_task(main())
    loop.run_forever()
