"""Dashboard — Startseite als responsives Card-Grid.

Kein abgespecktes Listen-Klon der System-Seite, sondern Karten mit großen
Werten, Level-Bars und Akzenten. Read-only aus den core-Modulen; die langsamen
Teile (Updates, Flatpak) laden async und füllen ihre Karten nach.
"""
from __future__ import annotations

import json
import threading
from pathlib import Path

import gi

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

from taninux.core import audio, network, packages, power, sysinfo


def _bytes(n: int) -> str:
    f = float(n)
    for unit in ("B", "KiB", "MiB", "GiB", "TiB"):
        if f < 1024 or unit == "TiB":
            return f"{f:.0f} {unit}" if unit == "B" else f"{f:.1f} {unit}"
        f /= 1024
    return f"{f:.1f} TiB"


def _uptime(secs: float) -> str:
    s = int(secs)
    d, s = divmod(s, 86400)
    h, s = divmod(s, 3600)
    m, _ = divmod(s, 60)
    parts = []
    if d:
        parts.append(f"{d}d")
    if h:
        parts.append(f"{h}h")
    parts.append(f"{m}m")
    return " ".join(parts)


# ----------------------------------------------------------- card builders

def _shell(icon: str, title: str) -> tuple[Gtk.Widget, Gtk.Box]:
    """Karten-Hülle (.card) + innerer Content-Box. Gibt (card, content)."""
    card = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
    card.add_css_class("card")
    card.set_valign(Gtk.Align.START)

    inner = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
    inner.set_margin_top(16)
    inner.set_margin_bottom(16)
    inner.set_margin_start(16)
    inner.set_margin_end(16)

    head = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
    img = Gtk.Image.new_from_icon_name(icon)
    img.add_css_class("dim-label")
    head.append(img)
    t = Gtk.Label(label=title, xalign=0.0)
    t.add_css_class("caption-heading")
    t.add_css_class("dim-label")
    head.append(t)
    inner.append(head)

    card.append(inner)
    return card, inner


def _metric(icon: str, title: str, value: str, *, sub: str | None = None,
            frac: float | None = None, value_css: str | None = None):
    """Stat-Karte: großer Wert + optional Untertitel + Level-Bar.
    Gibt (card, value_label, sub_label) — Labels für async-Updates."""
    card, inner = _shell(icon, title)

    v = Gtk.Label(label=value, xalign=0.0)
    v.add_css_class("title-2")
    if value_css:
        v.add_css_class(value_css)
    inner.append(v)

    s = Gtk.Label(label=sub or "", xalign=0.0)
    s.add_css_class("dim-label")
    s.add_css_class("caption")
    s.set_wrap(True)
    s.set_visible(bool(sub))
    inner.append(s)

    if frac is not None:
        lb = Gtk.LevelBar()
        lb.set_min_value(0.0)
        lb.set_max_value(1.0)
        lb.set_value(max(0.0, min(1.0, frac)))
        lb.set_margin_top(6)
        inner.append(lb)

    return card, v, s


def _info(icon: str, title: str, items: list[tuple[str, str]]) -> Gtk.Widget:
    """Info-Karte mit mehreren Label/Wert-Zeilen."""
    card, inner = _shell(icon, title)
    grid = Gtk.Grid()
    grid.set_row_spacing(4)
    grid.set_column_spacing(16)
    grid.set_margin_top(2)
    for r, (k, val) in enumerate(items):
        kl = Gtk.Label(label=k, xalign=0.0)
        kl.add_css_class("dim-label")
        vl = Gtk.Label(label=val, xalign=1.0, hexpand=True)
        vl.set_selectable(True)
        vl.set_ellipsize(3)  # END
        grid.attach(kl, 0, r, 1, 1)
        grid.attach(vl, 1, r, 1, 1)
    inner.append(grid)
    return card


def _timeshift_summary() -> tuple[str, str]:
    """(value, sub) aus der world-readable Timeshift-Config — ohne root."""
    cfg = Path("/etc/timeshift/timeshift.json")
    if not cfg.is_file():
        return "Not set up", "Timeshift not configured"
    try:
        data = json.loads(cfg.read_text())
    except (OSError, json.JSONDecodeError):
        return "—", "config unreadable"
    mode = "BTRFS" if data.get("btrfs_mode") == "true" else "rsync"
    sched = [k[len("schedule_"):] for k, v in data.items()
             if k.startswith("schedule_") and v == "true"]
    sub = ("schedule: " + ", ".join(sched)) if sched else "manual snapshots only"
    return mode, sub


def build() -> Gtk.Widget:
    flow = Gtk.FlowBox()
    flow.set_selection_mode(Gtk.SelectionMode.NONE)
    flow.set_homogeneous(True)
    flow.set_min_children_per_line(2)
    flow.set_max_children_per_line(2)
    flow.set_column_spacing(12)
    flow.set_row_spacing(12)
    flow.set_valign(Gtk.Align.START)

    # -- System (Info-Karte) ------------------------------------------
    flow.append(_info("computer-symbolic", "System", [
        ("Host", sysinfo.hostname()),
        ("Distro", sysinfo.distro_pretty()),
        ("Kernel", sysinfo.kernel_release()),
        ("Uptime", _uptime(sysinfo.uptime_seconds())),
    ]))

    # -- Memory -------------------------------------------------------
    mem = sysinfo.memory()
    total = mem.get("MemTotal", 0)
    used = total - mem.get("MemAvailable", 0)
    if total:
        flow.append(_metric(
            "drive-harddisk-symbolic", "Memory", _bytes(used),
            sub=f"of {_bytes(total)} used", frac=used / total,
        )[0])

    # -- CPU load -----------------------------------------------------
    la = sysinfo.load_avg()
    cpus = sysinfo.cpu_count()
    flow.append(_metric(
        "utilities-system-monitor-symbolic", "CPU load", f"{la[0]:.2f}",
        sub=f"{cpus} cores · 5m {la[1]:.2f} · 15m {la[2]:.2f}",
        frac=la[0] / cpus if cpus else 0,
    )[0])

    # -- Updates (async) ----------------------------------------------
    upd_card, upd_v, upd_s = _metric(
        "software-update-available-symbolic", "Updates", "…",
        sub="checking", value_css="accent",
    )
    flow.append(upd_card)

    # -- Packages (pacman sync, flatpak async) ------------------------
    pk_total = packages.pacman_total()
    pkg_card, pkg_v, pkg_s = _metric(
        "package-x-generic-symbolic", "Packages", str(pk_total),
        sub=f"{packages.pacman_foreign()} AUR · {packages.pacman_explicit()} explicit · flatpak …",
    )
    flow.append(pkg_card)

    # -- Backups (Timeshift, unprivilegiert aus config) ---------------
    bk_val, bk_sub = _timeshift_summary()
    flow.append(_metric("drive-multidisk-symbolic", "Backups", bk_val, sub=bk_sub)[0])

    # -- Network ------------------------------------------------------
    flow.append(_metric(
        "network-wireless-symbolic", "Network",
        network.active_summary(), sub="NetworkManager",
    )[0])

    # -- Audio --------------------------------------------------------
    ds = audio.get_default("sink")
    if ds:
        vol = audio.get_volume("sink", ds)
        muted = audio.get_muted("sink", ds)
        name = next((d.description for d in audio.list_devices("sink")
                     if d.name == ds), ds)
        flow.append(_metric(
            "audio-volume-high-symbolic", "Audio",
            "muted" if muted else f"{vol}%", sub=name,
        )[0])

    # -- Graphics -----------------------------------------------------
    gpus = sysinfo.gpus()
    if gpus:
        g0 = gpus[0]
        sub = g0.model[:42] + (f" · {g0.driver}" if g0.driver else "")
        flow.append(_metric(
            "video-display-symbolic", "Graphics", g0.vendor.upper(), sub=sub,
        )[0])

    # -- Storage ------------------------------------------------------
    disks = sysinfo.disks()
    if disks:
        total = sum(d.size_bytes for d in disks)
        flow.append(_metric(
            "drive-multidisk-symbolic", "Storage", _bytes(total),
            sub=f"{len(disks)} drive(s)",
        )[0])

    # -- Health -------------------------------------------------------
    failed = sysinfo.systemctl_failed()
    je = sysinfo.journal_errors_boot()
    if not failed:
        h_sub = "no failed units" + (f" · {je} journal errors" if je > 0 else "")
    else:
        h_sub = ", ".join(failed)
    flow.append(_metric(
        "emblem-ok-symbolic" if not failed else "dialog-warning-symbolic",
        "Health",
        "Healthy" if not failed else f"{len(failed)} failed",
        sub=h_sub,
        value_css="success" if not failed else "error",
    )[0])

    # -- async: Updates + Flatpak nachladen ---------------------------
    def worker() -> None:
        fp = packages.flatpak_apps()
        try:
            from taninux.update.sources import pacman as upd_src
            n = len(upd_src.fetch())
        except Exception:  # pragma: no cover
            n = -1

        def deliver() -> bool:
            if n < 0:
                upd_v.set_label("—")
                upd_s.set_visible(False)
            elif n == 0:
                upd_v.set_label("0")
                upd_v.remove_css_class("accent")
                upd_v.add_css_class("success")
                upd_s.set_label("up to date")
            else:
                upd_v.set_label(str(n))
                upd_s.set_label("updates available")
            fp_txt = "no flatpak" if fp < 0 else f"{fp} flatpak"
            pkg_s.set_label(
                f"{packages.pacman_foreign()} AUR · "
                f"{packages.pacman_explicit()} explicit · {fp_txt}"
            )
            return False

        GLib.idle_add(deliver)

    threading.Thread(target=worker, daemon=True).start()

    page = Adw.PreferencesPage()
    grp = Adw.PreferencesGroup()
    grp.add(flow)
    page.add(grp)
    return page
