"""
Wi-Fi operations via NetworkManager (`nmcli`) — the real backend behind the
Control Wi-Fi screens, which until now were pure mock ("Home 5G", simulated
connect).

dArkOS-RE (Debian 13 base) ships NetworkManager AND wpa_supplicant both
enabled; `nmcli` is the right tool (verified on-card: /usr/bin/nmcli present,
NetworkManager.service in multi-user.target.wants). We talk to it in terse
mode (`-t`) so output is machine-parseable.

Design split (so the logic is testable off-device, where nmcli is absent):
  • PURE PARSERS — `parse_wifi_list`, `parse_saved`, `parse_radio`,
    `signal_to_lead` — operate on captured nmcli text. 100% unit-testable.
  • THIN LIVE LAYER — `scan`, `connected_ssid`, `radio_on`, `connect`,
    `forget`, `set_radio` — shell out with a short timeout, tolerate a
    missing nmcli (dev box) by returning a sentinel so the screen falls
    back to its demo data instead of crashing.

`AAHEAD_FAKE_WIFI` (already used by the status bar) forces the dev path:
`available()` returns False, so screens keep their built-in demo networks.
"""

from __future__ import annotations

import os
import re
import subprocess
from typing import Optional


NMCLI = "nmcli"
_TIMEOUT = 6           # seconds; a wifi scan/query can take a few
_CONNECT_TIMEOUT = 45  # a real connect = associate + 4-way handshake + DHCP;
                       # on a USB dongle this is routinely 10-30s, NOT 6.

# On the R36S, nmcli needs root and the GUI runs as the unprivileged `ark`
# user — which has passwordless sudo (the same trick launch.sh uses for
# `sudo perfmax`). dArkOS's own Wifi.sh calls `sudo nmcli …` for every
# scan / connect / delete, so we mirror that exactly. `-n` (non-interactive)
# means: never block on a password prompt — on a dev box without passwordless
# sudo it fails fast, so `_run` returns None and the screen keeps demo data
# instead of hanging. Set AAHEAD_NMCLI_SUDO=0 to drop the prefix (already root,
# or a box where you don't want sudo).
_SUDO = [] if os.environ.get("AAHEAD_NMCLI_SUDO") == "0" else ["sudo", "-n"]


# ── pure parsers (testable; no device) ────────────────────────────────

def _split_terse(line: str) -> list[str]:
    r"""Split an `nmcli -t` line on ':' while respecting the '\:' escape
    nmcli uses for literal colons inside fields (SSIDs, security strings).
    Returns the unescaped fields."""
    out, cur, i = [], [], 0
    while i < len(line):
        ch = line[i]
        if ch == "\\" and i + 1 < len(line):
            cur.append(line[i + 1])      # unescape \x → x (incl. \:)
            i += 2
            continue
        if ch == ":":
            out.append("".join(cur)); cur = []
        else:
            cur.append(ch)
        i += 1
    out.append("".join(cur))
    return out


def signal_to_lead(signal) -> int:
    """nmcli SIGNAL (0-100) → 0-4 bars for the UI."""
    try:
        s = int(signal)
    except (TypeError, ValueError):
        return 0
    if s >= 80:
        return 4
    if s >= 55:
        return 3
    if s >= 30:
        return 2
    if s > 0:
        return 1
    return 0


def _band(freq) -> str:
    """Frequency (MHz) → band label."""
    try:
        f = int(freq)
    except (TypeError, ValueError):
        return ""
    return "5 GHz" if f >= 4900 else "2.4 GHz"


def parse_wifi_list(text: str) -> list[dict]:
    r"""Parse `nmcli -t -f IN-USE,SSID,SIGNAL,SECURITY,FREQ device wifi list`.
    Returns [{ssid, lead, band, security, open, connected}], de-duped by SSID
    keeping the strongest signal, blank SSIDs (hidden) dropped, sorted
    connected-first then signal desc."""
    best: dict[str, dict] = {}
    for raw in text.splitlines():
        raw = raw.strip()
        if not raw:
            continue
        f = _split_terse(raw)
        if len(f) < 5:
            continue
        in_use, ssid, signal, security, freq = f[0], f[1], f[2], f[3], f[4]
        if not ssid:
            continue                                  # hidden / blank
        sec = (security or "").strip()
        entry = {
            "ssid": ssid,
            "lead": signal_to_lead(signal),
            "band": _band(freq),
            "security": sec,
            "open": sec in ("", "--"),
            "connected": in_use.strip() in ("*", "yes"),
        }
        prev = best.get(ssid)
        if prev is None or entry["lead"] > prev["lead"] or entry["connected"]:
            best[ssid] = entry
    rows = list(best.values())
    rows.sort(key=lambda r: (not r["connected"], -r["lead"], r["ssid"].lower()))
    return rows


def parse_devices(text: str) -> list[tuple]:
    """Parse `nmcli -t -f DEVICE,TYPE device status` → [(device, type), …]."""
    out = []
    for raw in text.splitlines():
        f = _split_terse(raw.strip())
        if len(f) >= 2 and f[0]:
            out.append((f[0], f[1]))
    return out


def has_wifi_device(text: str) -> bool:
    """True iff any NetworkManager device is of type 'wifi' — i.e. the box has
    a (built-in or USB) Wi-Fi adapter. R36S has none until a dongle is added."""
    return any(typ == "wifi" for _dev, typ in parse_devices(text))


def parse_saved(text: str) -> set:
    """Parse `nmcli -t -f NAME,TYPE connection show` → set of saved Wi-Fi
    connection names (profiles we can connect to without a password / can
    forget)."""
    saved = set()
    for raw in text.splitlines():
        f = _split_terse(raw.strip())
        if len(f) >= 2 and f[1].endswith("wireless"):    # 802-11-wireless
            saved.add(f[0])
    return saved


def parse_radio(text: str) -> bool:
    """`nmcli radio wifi` → 'enabled' / 'disabled'."""
    return text.strip().lower().startswith("enable")


_IPV4 = re.compile(r"\b(\d{1,3}(?:\.\d{1,3}){3})\b")


def parse_ip(text: str) -> Optional[str]:
    """First non-loopback IPv4 from `nmcli -g IP4.ADDRESS …` output (lines may
    carry a `/prefix` mask, which we strip). None if nothing usable."""
    for raw in text.splitlines():
        m = _IPV4.search(raw)
        if m and not m.group(1).startswith("127."):
            return m.group(1)
    return None


# ── thin live layer (device; tolerant off-device) ────────────────────

def available() -> bool:
    """True iff we should talk to nmcli. False under AAHEAD_FAKE_WIFI or when
    the binary is absent (dev box) → callers use their demo data."""
    if os.environ.get("AAHEAD_FAKE_WIFI"):
        return False
    return _which(NMCLI) is not None


def _which(name: str) -> Optional[str]:
    for d in (os.environ.get("PATH") or "").split(os.pathsep):
        p = os.path.join(d, name)
        if os.path.isfile(p) and os.access(p, os.X_OK):
            return p
    return None


def _run_cp(args: list[str], timeout: float = _TIMEOUT):
    """Run an nmcli command, return the CompletedProcess (rc/stdout/stderr) or
    None if it couldn't run (missing binary, timeout). Never raises."""
    try:
        return subprocess.run([*_SUDO, NMCLI, *args], capture_output=True,
                              text=True, timeout=timeout)
    except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
        return None


def _run(args: list[str], timeout: float = _TIMEOUT) -> Optional[str]:
    """Run an nmcli command; return stdout, or None on any failure (missing
    binary, non-zero exit, timeout). Never raises — Control must not crash
    because the network stack hiccuped."""
    r = _run_cp(args, timeout)
    if r is None or r.returncode != 0:
        return None
    return r.stdout


def scan(rescan: bool = True) -> Optional[list[dict]]:
    """Real network list (connected/saved flags merged), or None when nmcli
    is unavailable so the screen keeps its demo data.

    `rescan=True` forces a fresh radio scan (slow, a few seconds — run it off
    the render thread); `rescan=False` returns NetworkManager's cached list
    (instant), good for the first paint."""
    if not available():
        return None
    lst = _run(["-t", "-f", "IN-USE,SSID,SIGNAL,SECURITY,FREQ",
                "device", "wifi", "list",
                "--rescan", "yes" if rescan else "no"])
    if lst is None:
        return None
    rows = parse_wifi_list(lst)
    saved = parse_saved(_run(["-t", "-f", "NAME,TYPE",
                              "connection", "show"]) or "")
    for r in rows:
        r["saved"] = r["ssid"] in saved
    return rows


def status() -> Optional[dict]:
    """Current Wi-Fi state for the home screen, or None when nmcli is
    unavailable (→ screen keeps its demo hero). Best-effort: ssid/lead/band
    come from the connected scan row, ip from a separate device query, both
    degrade to '' / None without failing the whole call."""
    if not available():
        return None
    rows = scan(rescan=False)            # cached → instant, no radio scan
    if rows is None:
        return None
    conn = next((r for r in rows if r["connected"]), None)
    ip = parse_ip(_run(["-g", "IP4.ADDRESS", "device", "show"]) or "")
    return {
        "radio": radio_on(),
        "connected": conn is not None,
        "ssid": conn["ssid"] if conn else None,
        "lead": conn["lead"] if conn else 0,
        "band": conn["band"] if conn else "",
        "ip": ip,
    }


def adapter_present() -> Optional[bool]:
    """Is there a Wi-Fi adapter at all? None when nmcli is unavailable."""
    if not available():
        return None
    out = _run(["-t", "-f", "DEVICE,TYPE", "device", "status"])
    return None if out is None else has_wifi_device(out)


def radio_on() -> Optional[bool]:
    out = _run(["radio", "wifi"])
    return None if out is None else parse_radio(out)


def set_radio(on: bool) -> bool:
    """Turn Wi-Fi on/off. Returns True on success."""
    return _run(["radio", "wifi", "on" if on else "off"]) is not None


def connect(ssid: str, password: Optional[str] = None) -> dict:
    """Connect to `ssid`. Returns {ok: bool, error: str|None}.

    When a password is given (a fresh, user-typed connect) we first DELETE any
    existing saved profile for the SSID. nmcli reuses a saved profile's stored
    secret on `device wifi connect`, so a profile left behind by an earlier
    failed attempt (e.g. with a wrong password) would keep failing auth even
    with the correct password typed now. Deleting forces nmcli to create a
    clean profile from the password we pass. (For a saved-network reconnect,
    password is None → we keep the saved profile and its secret.)"""
    if password:
        _run_cp(["connection", "delete", ssid], timeout=10)   # ignore result
    args = ["device", "wifi", "connect", ssid]
    if password:
        args += ["password", password]
    r = _run_cp(args, timeout=_CONNECT_TIMEOUT)
    if r is None:
        return {"ok": False, "error": "timed out — the dongle didn't associate"}
    if r.returncode == 0:
        return {"ok": True, "error": None}
    # surface the REAL nmcli error (last non-empty line of stderr/stdout)
    lines = [ln.strip() for ln in
             ((r.stderr or "") + "\n" + (r.stdout or "")).splitlines()
             if ln.strip()]
    return {"ok": False, "error": lines[-1] if lines else "connection failed"}


def forget(ssid: str) -> bool:
    """Delete the saved connection profile for `ssid`. True on success."""
    return _run(["connection", "delete", ssid]) is not None
