#!/usr/bin/env python3
"""NetworkManager backend for the Network quad's expanded settings.

Every subcommand prints JSON to stdout ({"ok": bool, ...} for mutations) and
diagnostics to stderr. Reads use `nmcli -g` (raw, unescaped single fields) and
`ip -j`; writes go through `nmcli connection modify/up`, so persistence and polkit
auth are handled by NetworkManager. Kept as one script so the UI has a single,
stable contract (see modules/network.py).

  adapters                              list manageable devices + their connection
  ipconfig <con>                        ipv4/ipv6 method/addresses/gateway/dns
  set-ip <con> ipv4|ipv6 <method> [addr_cidr] [gateway] [dns_space_sep]
  routes                                kernel routing table (read-only)
  vlans                                 configured VLAN connections
  vlan-add <parent_dev> <id> [name]     create + bring up a VLAN
  vlan-del <name>                       delete a VLAN connection
  vlan-up  <name>                       activate a VLAN connection
  dns                                   effective DNS servers per device
  set-dns  <con> ipv4|ipv6 <servers_space_sep> <yes|no ignore-auto>
"""

from __future__ import annotations

import json
import subprocess
import sys


def _run(argv: list[str], timeout: int = 20) -> tuple[bool, str, str]:
    try:
        p = subprocess.run(argv, capture_output=True, text=True, timeout=timeout)
    except (FileNotFoundError, subprocess.SubprocessError) as exc:
        return False, "", str(exc)
    return p.returncode == 0, p.stdout, p.stderr


def _get(field: str, con: str) -> str:
    ok, out, _ = _run(["nmcli", "-g", field, "connection", "show", con])
    return out.strip() if ok else ""


def _list(field: str, con: str) -> list[str]:
    raw = _get(field, con)
    return [v.strip() for v in raw.replace(",", " ").split() if v.strip()]


def _emit(obj) -> int:
    print(json.dumps(obj))
    return 0


def _ok(ok: bool, err: str = "") -> int:
    print(json.dumps({"ok": bool(ok), "error": err.strip()}))
    return 0 if ok else 1


# -- reads -----------------------------------------------------------------
def adapters() -> int:
    ok, out, _ = _run(["nmcli", "-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "device"])
    rows = []
    if ok:
        for line in out.splitlines():
            # DEVICE may contain escaped colons (bt MACs); split from the right on
            # the three trailing fields we know are colon-free-ish.
            parts = line.split(":")
            if len(parts) < 4:
                continue
            connection = parts[-1]
            state = parts[-2]
            typ = parts[-3]
            device = ":".join(parts[:-3]).replace("\\:", ":")
            if typ not in ("ethernet", "wifi"):
                continue
            rows.append({"device": device, "type": typ, "state": state,
                         "connection": connection.replace("\\:", ":")})
    return _emit(rows)


def ipconfig(con: str) -> int:
    def family(fam: str) -> dict:
        return {
            "method": _get(f"{fam}.method", con) or "auto",
            "addresses": _list(f"{fam}.addresses", con),
            "gateway": _get(f"{fam}.gateway", con),
            "dns": _list(f"{fam}.dns", con),
            "ignore_auto_dns": _get(f"{fam}.ignore-auto-dns", con) == "yes",
        }
    return _emit({"connection": con, "ipv4": family("ipv4"), "ipv6": family("ipv6")})


def routes() -> int:
    ok, out, err = _run(["ip", "-j", "route", "show"])
    if not ok:
        return _emit([])
    try:
        return _emit(json.loads(out))
    except json.JSONDecodeError:
        return _emit([])


def vlans() -> int:
    ok, out, _ = _run(["nmcli", "-t", "-f", "NAME,TYPE,DEVICE,ACTIVE", "connection", "show"])
    rows = []
    if ok:
        for line in out.splitlines():
            parts = line.rsplit(":", 3)
            if len(parts) < 4 or parts[1] != "vlan":
                continue
            name, _typ, device, active = parts
            rows.append({"name": name, "device": device, "active": active == "yes"})
    return _emit(rows)


def dns() -> int:
    """Effective DNS servers, per device, from `nmcli device show`."""
    ok, out, _ = _run(["nmcli", "-t", "-f",
                       "GENERAL.DEVICE,IP4.DNS,IP6.DNS", "device", "show"])
    devices: dict[str, dict] = {}
    cur = None
    if ok:
        for line in out.splitlines():
            if ":" not in line:
                continue
            key, _, val = line.partition(":")
            val = val.strip()
            if key == "GENERAL.DEVICE":
                cur = val
                devices.setdefault(cur, {"device": cur, "servers": []})
            elif cur and key.startswith(("IP4.DNS", "IP6.DNS")) and val:
                devices[cur]["servers"].append(val)
    return _emit([d for d in devices.values() if d["servers"]])


# -- writes ----------------------------------------------------------------
def set_ip(con: str, fam: str, method: str, addr: str = "",
           gateway: str = "", dns: str = "") -> int:
    if fam not in ("ipv4", "ipv6"):
        return _ok(False, "family must be ipv4 or ipv6")
    args = ["nmcli", "connection", "modify", con, f"{fam}.method", method]
    if method == "manual":
        args += [f"{fam}.addresses", addr or ""]
        args += [f"{fam}.gateway", gateway or ""]
        if dns:
            args += [f"{fam}.dns", dns]
    else:  # auto: clear any manual leftovers so DHCP is clean
        args += [f"{fam}.addresses", "", f"{fam}.gateway", ""]
    ok, _, err = _run(args)
    if ok:
        ok, _, err = _run(["nmcli", "connection", "up", con])
    return _ok(ok, err)


def set_dns(con: str, fam: str, servers: str, ignore_auto: str) -> int:
    if fam not in ("ipv4", "ipv6"):
        return _ok(False, "family must be ipv4 or ipv6")
    ok, _, err = _run(["nmcli", "connection", "modify", con,
                       f"{fam}.dns", servers,
                       f"{fam}.ignore-auto-dns", "yes" if ignore_auto == "yes" else "no"])
    if ok:
        ok, _, err = _run(["nmcli", "connection", "up", con])
    return _ok(ok, err)


def vlan_add(parent: str, vid: str, name: str = "") -> int:
    name = name or f"vlan{vid}"
    ok, _, err = _run(["nmcli", "connection", "add", "type", "vlan",
                       "con-name", name, "dev", parent, "id", str(vid)])
    if ok:
        ok, _, err = _run(["nmcli", "connection", "up", name])
    return _ok(ok, err)


def vlan_del(name: str) -> int:
    ok, _, err = _run(["nmcli", "connection", "delete", name])
    return _ok(ok, err)


def vlan_up(name: str) -> int:
    ok, _, err = _run(["nmcli", "connection", "up", name])
    return _ok(ok, err)


def main(argv: list[str]) -> int:
    if not argv:
        print("no subcommand", file=sys.stderr)
        return 2
    cmd, args = argv[0], argv[1:]
    table = {
        "adapters": lambda: adapters(),
        "ipconfig": lambda: ipconfig(args[0]),
        "set-ip": lambda: set_ip(*args),
        "routes": lambda: routes(),
        "vlans": lambda: vlans(),
        "vlan-add": lambda: vlan_add(*args),
        "vlan-del": lambda: vlan_del(args[0]),
        "vlan-up": lambda: vlan_up(args[0]),
        "dns": lambda: dns(),
        "set-dns": lambda: set_dns(*args),
    }
    fn = table.get(cmd)
    if fn is None:
        print(f"unknown subcommand: {cmd}", file=sys.stderr)
        return 2
    try:
        return fn()
    except TypeError as exc:
        print(f"bad arguments for {cmd}: {exc}", file=sys.stderr)
        return 2


if __name__ == "__main__":
    raise SystemExit(main(sys.argv[1:]))
