"""Network quad.

Compact: Wi-Fi enable + primary connection/IP (AstalNetwork + backend/network.sh).
Expanded: a small NetworkManager control panel over backend/nm.py, as four tabs —
  Adapters : each device foldout with editable IPv4/IPv6 (DHCP toggle, address,
             subnet-mask <-> CIDR, gateway); applied via `nmcli connection modify/up`
  Routes   : the kernel routing table (read-only; editing kernel routes needs root)
  VLAN     : list / create / activate / delete VLAN connections
  DNS      : effective servers per device + a per-connection override table
Every tab is an independently toggle-able feature.
"""

from __future__ import annotations

import ipaddress
import sys

import gi

gi.require_version("Gtk", "4.0")
from gi.repository import Gtk  # noqa: E402

from lib.proc import run_json
from module_base import Feature, ModuleContext, ModuleInstance, ModuleSpec
from paths import BACKEND_DIR

_NET = str(BACKEND_DIR / "network.sh")
_NM = str(BACKEND_DIR / "nm.py")


def _nm(args: list[str], cb) -> None:
    """Run a backend/nm.py subcommand expecting JSON on stdout."""
    run_json([sys.executable, _NM, *args], cb)


def _primary_ip(data) -> str:
    if not isinstance(data, list):
        return "—"
    for iface in data:
        if iface.get("ifname") == "lo":
            continue
        for a in iface.get("addr_info", []):
            if a.get("family") == "inet":
                return f"{a['local']}/{a.get('prefixlen', '')} ({iface.get('ifname')})"
    return "—"


# -- subnet mask <-> CIDR prefix ------------------------------------------
def _mask_to_prefix(mask: str) -> int | None:
    try:
        return ipaddress.IPv4Network(f"0.0.0.0/{mask.strip()}").prefixlen
    except (ipaddress.NetmaskValueError, ValueError):
        return None


def _prefix_to_mask(prefix: int) -> str:
    return str(ipaddress.IPv4Network(f"0.0.0.0/{prefix}").netmask)


class _Compact(Gtk.Box):
    def __init__(self, ctx: ModuleContext):
        super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=8)
        self.add_css_class("net-view")
        self.ctx = ctx
        self.net = ctx.services.network

        row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
        self._primary = Gtk.Label(label="…", xalign=0.0, hexpand=True)
        row.append(Gtk.Label(label=""))  # nf wifi
        row.append(self._primary)
        wifi = self.net.get_wifi() if self.net else None
        self._wifi_switch = Gtk.Switch(active=bool(wifi and wifi.get_enabled()))
        self._wifi_switch.set_tooltip_text("Wi-Fi")
        self._wifi_switch.connect("state-set", self._on_wifi_toggle)
        row.append(self._wifi_switch)
        self.append(row)

        self._ip = Gtk.Label(label="", xalign=0.0)
        self._ip.add_css_class("net-ip")
        self.append(self._ip)

        if self.net:
            self.net.connect("notify::primary", lambda *_: self.refresh())
            if wifi:
                wifi.connect("notify::ssid", lambda *_: self.refresh())
        self.refresh()

    def _on_wifi_toggle(self, _sw, value: bool) -> bool:
        wifi = self.net.get_wifi() if self.net else None
        if wifi:
            wifi.set_enabled(value)
        return False

    def refresh(self) -> None:
        wifi = self.net.get_wifi() if self.net else None
        if wifi and wifi.get_active_access_point():
            self._primary.set_text(f"{wifi.get_ssid() or '?'}  ·  {wifi.get_strength()}%")
        elif self.net and self.net.get_wired() and self.net.get_wired().get_internet():
            self._primary.set_text("Wired")
        else:
            self._primary.set_text("Disconnected")
        run_json([_NET, "ip"], lambda ok, d: self._ip.set_text(_primary_ip(d) if ok else "—"))


# -- shared little widgets -------------------------------------------------
def _clear(box: Gtk.Box) -> None:
    child = box.get_first_child()
    while child:
        box.remove(child)
        child = box.get_first_child()


def _field(placeholder: str, text: str = "") -> Gtk.Entry:
    e = Gtk.Entry(placeholder_text=placeholder, text=text, hexpand=True)
    e.add_css_class("net-entry")
    return e


def _labeled(label: str, widget: Gtk.Widget) -> Gtk.Widget:
    row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
    lbl = Gtk.Label(label=label, xalign=0.0)
    lbl.set_size_request(90, -1)
    row.append(lbl)
    row.append(widget)
    return row


def _pill(label: str, cb) -> Gtk.Button:
    b = Gtk.Button(label=label)
    b.add_css_class("quad-action")
    b.connect("clicked", lambda *_: cb())
    return b


# -- IPv4 / IPv6 editor for one connection --------------------------------
class _IPSection(Gtk.Box):
    """One address family's editor. IPv4 exposes a linked subnet-mask/CIDR pair;
    IPv6 exposes a prefix-length field. Fields are only sensitive in Manual mode."""

    def __init__(self, con: str, family: str, fam_data: dict, on_result):
        super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=6)
        self.con = con
        self.family = family
        self._on_result = on_result
        self.add_css_class("net-view")

        title = "IPv4" if family == "ipv4" else "IPv6"
        head = Gtk.CenterBox()
        head.set_start_widget(Gtk.Label(label=title, xalign=0.0))
        auto = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
        auto.append(Gtk.Label(label="Automatic"))
        self._dhcp = Gtk.Switch(active=(fam_data.get("method", "auto") != "manual"))
        self._dhcp.connect("state-set", self._on_mode)
        auto.append(self._dhcp)
        head.set_end_widget(auto)
        self.append(head)

        addr, prefix = self._split(fam_data.get("addresses", []))
        self._addr = _field("Address", addr)
        self._gw = _field("Gateway", fam_data.get("gateway", ""))

        self._fields = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
        self._fields.append(_labeled("Address", self._addr))
        if family == "ipv4":
            self._guard = False
            self._mask = _field("255.255.255.0", _prefix_to_mask(prefix) if prefix else "")
            self._cidr = _field("/24", f"/{prefix}" if prefix else "")
            self._cidr.set_size_request(70, -1)
            self._mask.connect("changed", self._sync_from_mask)
            self._cidr.connect("changed", self._sync_from_cidr)
            mask_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
            mask_row.append(self._mask)
            mask_row.append(self._cidr)
            self._fields.append(_labeled("Netmask", mask_row))
        else:
            self._prefix = _field("64", str(prefix) if prefix else "")
            self._fields.append(_labeled("Prefix", self._prefix))
        self._fields.append(_labeled("Gateway", self._gw))
        apply = Gtk.Button(label=f"Apply {title}")
        apply.add_css_class("enable-btn")
        apply.connect("clicked", lambda *_: self._apply())
        self._fields.append(apply)
        self.append(self._fields)

        self._set_manual_enabled(not self._dhcp.get_active())

    def _set_manual_enabled(self, on: bool) -> None:
        # The reset GTK theme doesn't dim insensitive widgets, so fade the manual
        # fields explicitly when DHCP/Automatic is active.
        self._fields.set_sensitive(on)
        self._fields.set_opacity(1.0 if on else 0.45)

    @staticmethod
    def _split(addresses: list[str]) -> tuple[str, int | None]:
        if not addresses:
            return "", None
        addr, _, prefix = addresses[0].partition("/")
        try:
            return addr, int(prefix)
        except ValueError:
            return addr, None

    def _on_mode(self, _sw, auto: bool) -> bool:
        self._set_manual_enabled(not auto)
        return False

    # keep the dotted mask and /CIDR fields in lock-step (IPv4 only)
    def _sync_from_mask(self, _e) -> None:
        if self._guard:
            return
        p = _mask_to_prefix(self._mask.get_text())
        if p is not None:
            self._guard = True
            self._cidr.set_text(f"/{p}")
            self._guard = False

    def _sync_from_cidr(self, _e) -> None:
        if self._guard:
            return
        raw = self._cidr.get_text().lstrip("/").strip()
        if raw.isdigit() and 0 <= int(raw) <= 32:
            self._guard = True
            self._mask.set_text(_prefix_to_mask(int(raw)))
            self._guard = False

    def _prefixlen(self) -> str:
        if self.family == "ipv4":
            raw = self._cidr.get_text().lstrip("/").strip()
            if raw.isdigit():
                return raw
            p = _mask_to_prefix(self._mask.get_text())
            return str(p) if p is not None else ""
        return self._prefix.get_text().strip()

    def _apply(self) -> None:
        if self._dhcp.get_active():
            args = ["set-ip", self.con, self.family, "auto"]
        else:
            addr, plen = self._addr.get_text().strip(), self._prefixlen()
            cidr = f"{addr}/{plen}" if addr and plen else addr
            args = ["set-ip", self.con, self.family, "manual",
                    cidr, self._gw.get_text().strip(), ""]
        _nm(args, lambda ok, d: self._on_result(
            ok and isinstance(d, dict) and d.get("ok"),
            (d.get("error") if isinstance(d, dict) else "") or ""))


class _AdaptersPage(Gtk.Box):
    def __init__(self):
        super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=6)
        self._status = Gtk.Label(xalign=0.0)
        self._status.add_css_class("net-ip")
        self._status.set_visible(False)
        self.append(self._status)
        self._list = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
        scroller = Gtk.ScrolledWindow(vexpand=True, hscrollbar_policy=Gtk.PolicyType.NEVER)
        scroller.set_child(self._list)
        self.append(scroller)
        self._reload()

    def _reload(self) -> None:
        _nm(["adapters"], self._populate)

    def _populate(self, ok: bool, adapters) -> None:
        _clear(self._list)
        if not ok or not isinstance(adapters, list) or not adapters:
            self._list.append(Gtk.Label(label="No manageable adapters", xalign=0.0))
            return
        for a in adapters:
            self._list.append(self._adapter_row(a))

    def _adapter_row(self, a: dict) -> Gtk.Widget:
        icon = "" if a.get("type") == "wifi" else ""  # nf wifi / ethernet
        exp = Gtk.Expander(
            label=f"{icon}  {a['device']}  ·  {a.get('connection') or '—'}  [{a.get('state')}]")
        exp.add_css_class("net-adapter")
        body = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
        body.set_margin_top(8)
        con = a.get("connection")
        if not con:
            body.append(Gtk.Label(label="No connection profile", xalign=0.0))
        else:
            loading = Gtk.Label(label="Loading…", xalign=0.0)
            body.append(loading)
            _nm(["ipconfig", con], lambda ok, d, b=body, l=loading, c=con:
                self._fill(ok, d, b, l, c))
        exp.set_child(body)
        return exp

    def _fill(self, ok, data, body, loading, con) -> None:
        body.remove(loading)
        if not ok or not isinstance(data, dict):
            body.append(Gtk.Label(label="Could not read config", xalign=0.0))
            return
        result = Gtk.Label(xalign=0.0)
        result.set_visible(False)

        def on_result(good: bool, err: str) -> None:
            result.set_text("Applied" if good else f"Failed: {err or 'error'}")
            result.set_visible(True)

        body.append(_IPSection(con, "ipv4", data.get("ipv4", {}), on_result))
        body.append(Gtk.Separator())
        body.append(_IPSection(con, "ipv6", data.get("ipv6", {}), on_result))
        body.append(result)


class _RoutesPage(Gtk.Box):
    def __init__(self):
        super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=4)
        header = Gtk.CenterBox()
        header.set_start_widget(Gtk.Label(label="Kernel routing table", xalign=0.0))
        header.set_end_widget(_pill("  Refresh", self._reload))
        self.append(header)
        self._list = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
        scroller = Gtk.ScrolledWindow(vexpand=True, hscrollbar_policy=Gtk.PolicyType.NEVER)
        scroller.set_child(self._list)
        self.append(scroller)
        self._reload()

    def _reload(self) -> None:
        _nm(["routes"], self._populate)

    def _populate(self, ok: bool, routes) -> None:
        _clear(self._list)
        if not ok or not isinstance(routes, list) or not routes:
            self._list.append(Gtk.Label(label="No routes", xalign=0.0))
            return
        for r in routes:
            self._list.append(Gtk.Label(label=self._fmt(r), xalign=0.0,
                                        selectable=True, wrap=True))

    @staticmethod
    def _fmt(r: dict) -> str:
        dst = r.get("dst", "?")
        via = f" via {r['gateway']}" if r.get("gateway") else ""
        dev = f" dev {r['dev']}" if r.get("dev") else ""
        metric = f"  metric {r['metric']}" if r.get("metric") is not None else ""
        proto = f"  ({r['proto']})" if r.get("proto") else ""
        return f"{dst}{via}{dev}{metric}{proto}"


class _VlanPage(Gtk.Box):
    def __init__(self):
        super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=8)
        self._adapters: list[dict] = []

        add = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
        self._parent = Gtk.DropDown.new_from_strings(["…"])
        self._vid = _field("VLAN ID")
        self._vid.set_size_request(90, -1)
        add.append(self._parent)
        add.append(self._vid)
        add.append(_pill("Add", self._add))
        self.append(_labeled("New VLAN", add))

        self._status = Gtk.Label(xalign=0.0)
        self._status.add_css_class("net-ip")
        self._status.set_visible(False)
        self.append(self._status)

        self._list = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4)
        scroller = Gtk.ScrolledWindow(vexpand=True, hscrollbar_policy=Gtk.PolicyType.NEVER)
        scroller.set_child(self._list)
        self.append(scroller)

        _nm(["adapters"], self._set_adapters)
        self._reload()

    def _set_adapters(self, ok, adapters) -> None:
        self._adapters = adapters if ok and isinstance(adapters, list) else []
        names = [a["device"] for a in self._adapters] or ["(no devices)"]
        self._parent.set_model(Gtk.StringList.new(names))

    def _reload(self) -> None:
        _nm(["vlans"], self._populate)

    def _populate(self, ok, vlans) -> None:
        _clear(self._list)
        if not ok or not isinstance(vlans, list) or not vlans:
            self._list.append(Gtk.Label(label="No VLANs configured", xalign=0.0))
            return
        for v in vlans:
            self._list.append(self._vlan_row(v))

    def _vlan_row(self, v: dict) -> Gtk.Widget:
        row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
        row.add_css_class("net-row")
        state = "active" if v.get("active") else "inactive"
        row.append(Gtk.Label(label=f"{v['name']}  ·  {v.get('device') or '—'}  [{state}]",
                              xalign=0.0, hexpand=True))
        if not v.get("active"):
            row.append(_pill("Up", lambda n=v["name"]: self._act("vlan-up", n)))
        row.append(_pill("Delete", lambda n=v["name"]: self._act("vlan-del", n)))
        return row

    def _add(self) -> None:
        idx = self._parent.get_selected()
        vid = self._vid.get_text().strip()
        if idx < 0 or idx >= len(self._adapters) or not vid.isdigit():
            self._flash("Pick a parent device and numeric VLAN ID")
            return
        parent = self._adapters[idx]["device"]
        _nm(["vlan-add", parent, vid], self._after_write)

    def _act(self, cmd: str, name: str) -> None:
        _nm([cmd, name], self._after_write)

    def _after_write(self, ok, d) -> None:
        good = ok and isinstance(d, dict) and d.get("ok")
        self._flash("Done" if good else
                    f"Failed: {(d.get('error') if isinstance(d, dict) else '') or 'error'}")
        self._reload()

    def _flash(self, msg: str) -> None:
        self._status.set_text(msg)
        self._status.set_visible(True)


class _DnsPage(Gtk.Box):
    def __init__(self):
        super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=8)
        self._adapters: list[dict] = []

        self.append(Gtk.Label(label="Effective servers", xalign=0.0))
        self._effective = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
        self.append(self._effective)

        self.append(Gtk.Separator())
        self.append(Gtk.Label(label="Override", xalign=0.0))
        self._conn = Gtk.DropDown.new_from_strings(["…"])
        self.append(_labeled("Connection", self._conn))
        self._servers = _field("1.1.1.1 8.8.8.8")
        self.append(_labeled("Servers", self._servers))
        ignore = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
        ignore.append(Gtk.Label(label="Ignore automatic", hexpand=True, xalign=0.0))
        self._ignore = Gtk.Switch(active=True)
        ignore.append(self._ignore)
        self.append(ignore)
        self.append(_pill("Apply DNS", self._apply))
        self._status = Gtk.Label(xalign=0.0)
        self._status.add_css_class("net-ip")
        self._status.set_visible(False)
        self.append(self._status)

        _nm(["adapters"], self._set_adapters)
        _nm(["dns"], self._show_effective)

    def _set_adapters(self, ok, adapters) -> None:
        self._adapters = [a for a in (adapters or []) if a.get("connection")] \
            if ok and isinstance(adapters, list) else []
        names = [f"{a['connection']} ({a['device']})" for a in self._adapters] or ["(none)"]
        self._conn.set_model(Gtk.StringList.new(names))

    def _show_effective(self, ok, data) -> None:
        _clear(self._effective)
        if not ok or not isinstance(data, list) or not data:
            self._effective.append(Gtk.Label(label="—", xalign=0.0))
            return
        for d in data:
            self._effective.append(Gtk.Label(
                label=f"{d['device']}:  {', '.join(d.get('servers', []))}",
                xalign=0.0, selectable=True))

    def _apply(self) -> None:
        idx = self._conn.get_selected()
        if idx < 0 or idx >= len(self._adapters):
            self._flash("Pick a connection")
            return
        con = self._adapters[idx]["connection"]
        servers = self._servers.get_text().strip()
        ign = "yes" if self._ignore.get_active() else "no"
        _nm(["set-dns", con, "ipv4", servers, ign], self._after)

    def _after(self, ok, d) -> None:
        good = ok and isinstance(d, dict) and d.get("ok")
        self._flash("Applied" if good else
                    f"Failed: {(d.get('error') if isinstance(d, dict) else '') or 'error'}")
        _nm(["dns"], self._show_effective)

    def _flash(self, msg: str) -> None:
        self._status.set_text(msg)
        self._status.set_visible(True)


class _Expanded(Gtk.Box):
    def __init__(self, ctx: ModuleContext):
        super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=8)
        self.add_css_class("net-view")
        self.ctx = ctx

        self.stack = Gtk.Stack(vexpand=True)
        switcher = Gtk.StackSwitcher(stack=self.stack)
        switcher.add_css_class("net-switcher")
        self.append(switcher)
        self.append(self.stack)

        f = ctx.feature
        if f("adapters", True):
            self.stack.add_titled(_AdaptersPage(), "adapters", "Adapters")
        if f("routes", True):
            self.stack.add_titled(_RoutesPage(), "routes", "Routes")
        if f("vlan", True):
            self.stack.add_titled(_VlanPage(), "vlan", "VLAN")
        if f("dns", True):
            self.stack.add_titled(_DnsPage(), "dns", "DNS")


def build(ctx: ModuleContext) -> ModuleInstance:
    # Feature toggles rebuild the whole card via QuadCard, so _Expanded is recreated
    # with the enabled tabs — no in-place page rebuild needed here.
    compact = _Compact(ctx)
    expanded = _Expanded(ctx)
    return ModuleInstance(compact=compact, expanded=expanded)


SPEC = ModuleSpec(
    id="network",
    title="Network",
    icon="",  # nf-md-lan
    build=build,
    default_enabled=True,
    features=[Feature("adapters", "Adapters", True),
              Feature("routes", "Routes", True),
              Feature("vlan", "VLAN", True),
              Feature("dns", "DNS", True)],
)
