"""
SystemStatus — live readings for the top-bar chrome (clock, battery, wifi).

Tolerant by design: when running off-device (dev box / pytest) the sysfs
files don't exist → we return sane defaults so the UI keeps rendering. The
real R36S exposes `/sys/class/power_supply/battery/capacity` (some distros
also use `BAT0/capacity`).

Two env vars freeze the values for deterministic screenshots / tests:
    AAHEAD_FAKE_CLOCK    — e.g. "14:32"
    AAHEAD_FAKE_BATTERY  — e.g. "82" (clamped 0..100; non-numeric → live)

Wired by `gui.widgets.chrome.status_bar` and `gui.widgets.nav.NavBar.draw`
via a module-level singleton (`get_status()`). Screens that want a snapshot
use the same singleton — no per-frame sysfs reads beyond the 10 s cache.
"""

from __future__ import annotations

import os
import time
from datetime import datetime
from typing import Optional, Sequence

DEFAULT_BATTERY_PATHS: tuple = (
    "/sys/class/power_supply/battery/capacity",
    "/sys/class/power_supply/BAT0/capacity",
)
DEFAULT_STATUS_PATHS: tuple = (
    "/sys/class/power_supply/battery/status",
    "/sys/class/power_supply/BAT0/status",
)
DEFAULT_BATTERY = 82           # off-device demo value (matches the old mock so
                               # nothing visual jumps the first time we wire this)

FAKE_CLOCK_ENV = "AAHEAD_FAKE_CLOCK"
FAKE_BATTERY_ENV = "AAHEAD_FAKE_BATTERY"
FAKE_WIFI_ENV = "AAHEAD_FAKE_WIFI"
FAKE_BT_ENV = "AAHEAD_FAKE_BT"

DEFAULT_NET_DIR = "/sys/class/net"
DEFAULT_BT_DIR = "/sys/class/bluetooth"


# ── pure helpers (independently testable) ─────────────────────────────


def _format_clock(dt: datetime) -> str:
    """24-hour HH:MM, zero-padded. Local time."""
    return dt.strftime("%H:%M")


def now_hhmm() -> str:
    """Wall-clock HH:MM. Respects AAHEAD_FAKE_CLOCK for determinism."""
    fake = os.environ.get(FAKE_CLOCK_ENV)
    if fake:
        return fake
    return _format_clock(datetime.now())


def battery_percent(
    paths: Sequence[str] = DEFAULT_BATTERY_PATHS,
    default: int = DEFAULT_BATTERY,
) -> int:
    """First readable path under `paths` wins. Missing / garbage / out-of-range
    contents → `default`. Respects AAHEAD_FAKE_BATTERY (clamped 0..100; a
    non-numeric value is ignored so the env can't ship a broken UI)."""
    fake = os.environ.get(FAKE_BATTERY_ENV)
    if fake is not None and fake.lstrip("-").isdigit():
        return max(0, min(100, int(fake)))
    for p in paths:
        try:
            with open(p, encoding="utf-8") as fh:
                v = int(fh.read().strip())
                if 0 <= v <= 100:
                    return v
        except (OSError, ValueError):
            continue
    return default


def is_charging(paths: Sequence[str] = DEFAULT_STATUS_PATHS) -> bool:
    """True if any of `paths` reports a charging-class status."""
    for p in paths:
        try:
            with open(p, encoding="utf-8") as fh:
                v = fh.read().strip().lower()
            return v in ("charging", "full")
        except OSError:
            continue
    return False


# ── wifi (wireless interface presence + link state) ──────────────────


def _is_wireless_iface(iface_path: str) -> bool:
    """A wireless interface is identified by a `wireless/` subdir OR a
    `phy80211` symlink under `/sys/class/net/<iface>/`."""
    if os.path.isdir(os.path.join(iface_path, "wireless")):
        return True
    if os.path.lexists(os.path.join(iface_path, "phy80211")):
        return True
    return False


def wifi_up(net_dir: str = DEFAULT_NET_DIR) -> bool:
    """True iff any wireless interface under `net_dir` reports operstate=up
    AND carrier=1 (i.e. actively associated with a network, not just powered).
    Respects AAHEAD_FAKE_WIFI (`1`/`true`/`yes`/`on`/`up` → True, anything else
    → False) for deterministic screenshots."""
    fake = os.environ.get(FAKE_WIFI_ENV)
    if fake is not None:
        return fake.strip().lower() in ("1", "true", "yes", "on", "up")
    try:
        ifaces = os.listdir(net_dir)
    except OSError:
        return False
    for iface in ifaces:
        if iface == "lo":
            continue
        ipath = os.path.join(net_dir, iface)
        if not _is_wireless_iface(ipath):
            continue
        try:
            with open(os.path.join(ipath, "operstate"), encoding="utf-8") as fh:
                state = fh.read().strip().lower()
            with open(os.path.join(ipath, "carrier"), encoding="utf-8") as fh:
                carrier = fh.read().strip()
        except OSError:
            continue
        if state == "up" and carrier == "1":
            return True
    return False


# ── bluetooth (HCI adapter presence) ──────────────────────────────────


def bt_adapter_present(bt_dir: str = DEFAULT_BT_DIR) -> bool:
    """True iff any HCI adapter (hci0, hci1, …) is exposed under `bt_dir`.

    The R36S `csr-fix` BT donglo creates `/sys/class/bluetooth/hci0`; off-device
    the directory doesn't exist → False. We deliberately do NOT spawn
    bluetoothctl here — checking sysfs is sub-millisecond and doesn't need
    DBus / root. "Connected to a specific device" is a separate, heavier
    signal (see csr-fix `aactl bt`).

    Respects AAHEAD_FAKE_BT (`1`/`true`/`yes`/`on` → True, else → False)."""
    fake = os.environ.get(FAKE_BT_ENV)
    if fake is not None:
        return fake.strip().lower() in ("1", "true", "yes", "on")
    try:
        for entry in os.listdir(bt_dir):
            if entry.startswith("hci"):
                return True
    except OSError:
        return False
    return False


# ── cached snapshot ───────────────────────────────────────────────────


class SystemStatus:
    """One instance per App. Battery is cached for `BATTERY_REFRESH_S` seconds
    (sysfs reads are cheap but we don't need them per-frame at 60 Hz). Clock
    is read on every access — `datetime.now()` is trivially fast."""

    BATTERY_REFRESH_S = 10.0

    def __init__(
        self,
        battery_paths: Sequence[str] = DEFAULT_BATTERY_PATHS,
        status_paths: Sequence[str] = DEFAULT_STATUS_PATHS,
        net_dir: str = DEFAULT_NET_DIR,
        bt_dir: str = DEFAULT_BT_DIR,
        default_battery: int = DEFAULT_BATTERY,
    ):
        self._battery_paths = tuple(battery_paths)
        self._status_paths = tuple(status_paths)
        self._net_dir = net_dir
        self._bt_dir = bt_dir
        self._default_battery = default_battery
        self._battery: Optional[int] = None
        self._charging: bool = False
        self._wifi: Optional[bool] = None
        self._bt: Optional[bool] = None
        self._next_refresh_at: float = 0.0

    @property
    def clock(self) -> str:
        return now_hhmm()

    def _maybe_refresh(self) -> None:
        now = time.monotonic()
        if self._battery is None or now >= self._next_refresh_at:
            self._battery = battery_percent(self._battery_paths, self._default_battery)
            self._charging = is_charging(self._status_paths)
            self._wifi = wifi_up(self._net_dir)
            self._bt = bt_adapter_present(self._bt_dir)
            self._next_refresh_at = now + self.BATTERY_REFRESH_S

    @property
    def battery(self) -> int:
        self._maybe_refresh()
        assert self._battery is not None    # populated by _maybe_refresh
        return self._battery

    @property
    def charging(self) -> bool:
        self._maybe_refresh()
        return self._charging

    @property
    def wifi(self) -> bool:
        self._maybe_refresh()
        assert self._wifi is not None
        return self._wifi

    @property
    def bt(self) -> bool:
        self._maybe_refresh()
        assert self._bt is not None
        return self._bt


# ── singleton (chrome / nav default to this) ──────────────────────────


_singleton: Optional[SystemStatus] = None


def get_status() -> SystemStatus:
    """Module-level lazy singleton. Tests that need a fresh instance can call
    `reset_status()` or instantiate `SystemStatus(...)` directly."""
    global _singleton
    if _singleton is None:
        _singleton = SystemStatus()
    return _singleton


def reset_status() -> None:
    """Drop the singleton — for tests that change env vars between cases."""
    global _singleton
    _singleton = None
