#!/usr/bin/env python3
#--------------------------------------------------------------------------------------------------------
# Name: Linux Lite - About Linux Lite
# Architecture: amd64
# Author: Jerry Bezencon
# Website: https://www.linuxliteos.com
# Language: Python/GTK4
# Licence: GPLv2
#--------------------------------------------------------------------------------------------------------

import os
import sys
import re
import glob
import socket
import subprocess
from pathlib import Path

os.environ['GSK_RENDERER'] = 'cairo'

import gi
gi.require_version("Gtk", "4.0")
gi.require_version("Gdk", "4.0")
from gi.repository import Gtk, Gdk, Gio, GLib, Pango

APP_ID = "com.linuxlite.About"
APP_NAME = "About Linux Lite"

# Icon paths (bundled in /usr/share/aboutlite)
ICON_DIR = "/usr/share/aboutlite"
ICONS = {
    "logo":        f"{ICON_DIR}/logo.svg",
    "cpu":         f"{ICON_DIR}/cpu.svg",
    "memory":      f"{ICON_DIR}/memory.svg",
    "storage":     f"{ICON_DIR}/storage.svg",
    "gpu":         f"{ICON_DIR}/gpu.svg",
    "network":     f"{ICON_DIR}/network.svg",
    "system":      f"{ICON_DIR}/system.svg",
    "intel":       f"{ICON_DIR}/icon-intel.svg",
    "amd":         f"{ICON_DIR}/icon-amd.svg",
    "nvidia":      f"{ICON_DIR}/icon-nvidia.svg",
    "motherboard": f"{ICON_DIR}/motherboard.svg",
    "display":     f"{ICON_DIR}/display.svg",
    "audio":       f"{ICON_DIR}/audio.svg",
    "desktop":     f"{ICON_DIR}/desktop.svg",
    "battery":     f"{ICON_DIR}/battery.svg",
}


# ── System info helpers ──────────────────────────────────────────────

def run_cmd(cmd):
    try:
        r = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
        return r.stdout.strip()
    except Exception:
        return ""


def get_ll_version():
    try:
        return Path("/etc/llver").read_text().strip()
    except Exception:
        pass
    try:
        for line in Path("/etc/os-release").read_text().splitlines():
            if line.startswith("PRETTY_NAME="):
                return line.split("=", 1)[1].strip('"')
    except Exception:
        pass
    return "Linux Lite"


def get_kernel():
    return run_cmd(["uname", "-r"])


def get_uptime():
    try:
        seconds = float(Path("/proc/uptime").read_text().split()[0])
        days = int(seconds // 86400)
        hours = int((seconds % 86400) // 3600)
        mins = int((seconds % 3600) // 60)
        parts = []
        if days:
            parts.append(f"{days}d")
        if hours:
            parts.append(f"{hours}h")
        parts.append(f"{mins}m")
        return " ".join(parts)
    except Exception:
        return ""


def get_cpu_info():
    model = ""
    cores = 0
    threads = 0
    try:
        for line in Path("/proc/cpuinfo").read_text().splitlines():
            if line.startswith("model name") and not model:
                model = line.split(":", 1)[1].strip()
            if line.startswith("processor"):
                threads += 1
        # Physical cores
        cores_text = run_cmd(["nproc", "--all"])
        cores = int(cores_text) if cores_text.isdigit() else threads
        # Try lscpu for physical cores
        lscpu = run_cmd(["lscpu"])
        for line in lscpu.splitlines():
            if "Core(s) per socket:" in line:
                per_socket = int(line.split(":")[1].strip())
            if "Socket(s):" in line:
                sockets = int(line.split(":")[1].strip())
        try:
            cores = per_socket * sockets
        except Exception:
            pass
    except Exception:
        pass
    # Clean up model name
    model = re.sub(r'\s+', ' ', model)
    return model, cores, threads


def get_memory_info():
    total = 0
    available = 0
    try:
        for line in Path("/proc/meminfo").read_text().splitlines():
            if line.startswith("MemTotal:"):
                total = int(line.split()[1])
            elif line.startswith("MemAvailable:"):
                available = int(line.split()[1])
    except Exception:
        pass
    return total, available


def format_size_kb(kb):
    if kb >= 1048576:
        return f"{kb / 1048576:.1f} GB"
    elif kb >= 1024:
        return f"{kb / 1024:.0f} MB"
    return f"{kb} KB"


def format_bytes(b):
    if b >= 1099511627776:
        return f"{b / 1099511627776:.1f} TB"
    if b >= 1073741824:
        return f"{b / 1073741824:.1f} GB"
    if b >= 1048576:
        return f"{b / 1048576:.1f} MB"
    return f"{b} B"


def get_storage_info():
    drives = []
    try:
        # Get physical disks
        lsblk = run_cmd(["lsblk", "-d", "-n", "-o", "NAME,SIZE,MODEL,TYPE"])
        for line in lsblk.splitlines():
            parts = line.split(None, 3)
            if len(parts) >= 2:
                name = parts[0]
                size = parts[1]
                dtype = parts[-1] if len(parts) >= 4 else parts[2] if len(parts) >= 3 else ""
                model = ""
                if len(parts) == 4:
                    model = parts[2]
                elif len(parts) == 3 and parts[2] not in ("disk", "loop", "rom"):
                    model = parts[2]
                if dtype == "loop" or name.startswith("loop") or name.startswith("zram"):
                    continue
                if size == "0B":
                    continue
                drives.append({"name": f"/dev/{name}", "size": size, "model": model,
                               "total": 0, "used": 0, "fraction": 0.0})

        # Get mount usage from df for each drive's partitions
        df_output = run_cmd(["df", "-B1", "--output=source,size,used,target"])
        for drive in drives:
            dev = drive["name"]
            total_bytes = 0
            used_bytes = 0
            for line in df_output.splitlines()[1:]:
                parts = line.split()
                if len(parts) >= 4 and parts[0].startswith(dev):
                    total_bytes += int(parts[1])
                    used_bytes += int(parts[2])
            if total_bytes > 0:
                drive["total"] = total_bytes
                drive["used"] = used_bytes
                drive["fraction"] = used_bytes / total_bytes
    except Exception:
        pass
    return drives


def get_gpu_info():
    """Return list of dicts: {name, vram} with marketing names and VRAM where possible."""
    gpus = []

    # Try nvidia-smi first for NVIDIA GPUs
    nvidia_gpus = {}
    try:
        nv = run_cmd(["nvidia-smi", "--query-gpu=name,memory.total", "--format=csv,noheader,nounits"])
        for line in nv.splitlines():
            parts = [p.strip() for p in line.split(",")]
            if len(parts) >= 2:
                name = parts[0]
                vram_mb = int(float(parts[1]))
                vram = f"{vram_mb // 1024} GB" if vram_mb >= 1024 else f"{vram_mb} MB"
                nvidia_gpus[name.lower()] = {"name": name, "vram": vram}
    except Exception:
        pass

    # Try glxinfo as fallback for name/vram
    glx_name = ""
    glx_vram = ""
    try:
        glx = run_cmd(["glxinfo", "-B"])
        for line in glx.splitlines():
            if "OpenGL renderer string:" in line:
                glx_name = line.split(":", 1)[1].strip()
                glx_name = re.sub(r'\s*/.*$', '', glx_name)
            if "Video memory:" in line:
                glx_vram = line.split(":", 1)[1].strip()
    except Exception:
        pass

    # Get VRAM from sysfs (AMD cards)
    sysfs_vram = {}
    try:
        for card_dir in sorted(glob.glob("/sys/class/drm/card[0-9]*/device")):
            vram_file = os.path.join(card_dir, "mem_info_vram_total")
            if os.path.exists(vram_file):
                vram_bytes = int(Path(vram_file).read_text().strip())
                card_name = os.path.basename(os.path.dirname(card_dir))
                sysfs_vram[card_name] = format_bytes(vram_bytes)
    except Exception:
        pass

    # Parse lspci for GPU slots
    try:
        lspci = run_cmd(["lspci"])
        card_idx = 0
        for line in lspci.splitlines():
            lower = line.lower()
            if "vga" not in lower and "3d controller" not in lower and "display controller" not in lower:
                continue

            match = re.search(r':\s*(VGA|3D|Display)\s+(?:compatible\s+)?controller:\s*(.*)', line, re.IGNORECASE)
            pci_name = match.group(2).strip() if match else line.split(":", 2)[-1].strip()
            slot = line.split()[0] if line.split() else ""

            # Try to get subsystem (marketing) name from lspci -v
            short_name = ""
            try:
                detail = run_cmd(["lspci", "-v", "-s", slot])
                for dline in detail.splitlines():
                    if "Subsystem:" in dline:
                        sub = dline.split(":", 1)[1].strip()
                        sub = re.sub(r'^(NVIDIA|AMD|Intel)\s+(Corporation\s+)?', '', sub, flags=re.IGNORECASE).strip()
                        gpu_keywords = ("geforce", "radeon", "quadro", "rtx", "gtx", "uhd", "iris", "vega", "navi", "tesla", "titan", "rx ", "hd ")
                        if sub and any(kw in sub.lower() for kw in gpu_keywords):
                            short_name = sub
                        break
            except Exception:
                pass

            # Extract marketing name from PCI bracketed text e.g. [Radeon Vega Series]
            bracket_name = ""
            brackets = re.findall(r'\[([^\]]+)\]', pci_name)
            for b in brackets:
                if b in ("AMD/ATI", "AMD", "Intel"):
                    continue
                bracket_name = b
                break

            # Determine best name: nvidia-smi > subsystem > bracket > glxinfo > lspci raw
            final_name = ""
            vram = ""

            if nvidia_gpus:
                for nk, nv_info in nvidia_gpus.items():
                    if any(word in pci_name.lower() for word in nk.split()) or any(word in lower for word in nk.split()):
                        final_name = nv_info["name"]
                        vram = nv_info["vram"]
                        break

            if not final_name and short_name:
                final_name = short_name
            if not final_name and bracket_name:
                final_name = bracket_name
            if not final_name and glx_name and card_idx == 0:
                final_name = glx_name
            if not final_name:
                cleaned = re.sub(r'^(NVIDIA|AMD|Intel)\s+(Corporation\s+)?', '', pci_name, flags=re.IGNORECASE).strip()
                cleaned = re.sub(r'\[.*?\]', '', cleaned).strip()
                final_name = cleaned if cleaned else pci_name

            if not vram and glx_vram and card_idx == 0:
                vram = glx_vram
            if not vram:
                card_key = f"card{card_idx}"
                if card_key in sysfs_vram:
                    vram = sysfs_vram[card_key]

            # Clean up the final name
            if " / " in final_name:
                final_name = final_name.split(" / ")[0].strip()
            final_name = re.sub(r'\s*\(rev\s+\w+\)', '', final_name).strip()
            final_name = re.sub(r'Advanced Micro Devices,?\s*Inc\.?\s*', '', final_name).strip()
            final_name = re.sub(r'\[AMD(/ATI)?\]\s*', '', final_name).strip()
            final_name = re.sub(r'^\w+\s+(Radeon|GeForce|Quadro|UHD|Iris|Intel\s+HD)', r'\1', final_name).strip()

            # If the name is just "Device XXXX" (unknown PCI ID), use a friendly name
            if re.match(r'^Device\s+[0-9a-fA-F]+$', final_name):
                brand = detect_brand(pci_name)
                if brand == "amd":
                    final_name = "AMD Radeon Integrated Graphics"
                elif brand == "intel":
                    final_name = "Intel Integrated Graphics"
                elif brand == "nvidia":
                    final_name = "NVIDIA Graphics"
                else:
                    final_name = "Integrated Graphics"

            gpus.append({"name": final_name, "vram": vram})
            card_idx += 1
    except Exception:
        pass

    return gpus


def get_local_ip():
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.connect(("8.8.8.8", 80))
        ip = s.getsockname()[0]
        s.close()
        return ip
    except Exception:
        return "Not connected"


def get_hostname():
    try:
        return socket.gethostname()
    except Exception:
        return ""


def detect_brand(text):
    t = text.lower()
    if "intel" in t:
        return "intel"
    if "amd" in t or "radeon" in t:
        return "amd"
    if "nvidia" in t or "geforce" in t or "quadro" in t:
        return "nvidia"
    return None


def get_motherboard_info():
    """Return (manufacturer, product) from DMI sysfs."""
    manufacturer = ""
    product = ""
    try:
        p = Path("/sys/devices/virtual/dmi/id/board_vendor")
        if p.exists():
            manufacturer = p.read_text().strip()
    except Exception:
        pass
    try:
        p = Path("/sys/devices/virtual/dmi/id/board_name")
        if p.exists():
            product = p.read_text().strip()
    except Exception:
        pass
    return manufacturer, product


def get_display_info():
    """Return list of dicts: {name, resolution, refresh}."""
    displays = []
    try:
        xrandr = run_cmd(["xrandr", "--current"])
        for line in xrandr.splitlines():
            # Match connected outputs with resolution
            m = re.match(r'^(\S+)\s+connected\s+(?:primary\s+)?(\d+x\d+)\+', line)
            if m:
                name = m.group(1)
                res = m.group(2)
                # Find refresh rate from the mode line that has * (current)
                refresh = ""
                # Look at subsequent lines for the active mode
                for modeline in xrandr.splitlines():
                    if res in modeline and "*" in modeline:
                        rm = re.search(r'(\d+\.\d+)\s*\*', modeline)
                        if rm:
                            refresh = f"{float(rm.group(1)):.0f} Hz"
                        break
                displays.append({"name": name, "resolution": res, "refresh": refresh})
    except Exception:
        pass
    return displays


def get_audio_info():
    """Return list of audio device names from lspci."""
    devices = []
    try:
        lspci = run_cmd(["lspci"])
        for line in lspci.splitlines():
            if "audio" in line.lower():
                match = re.search(r':\s*(?:Audio device|Multimedia audio controller):\s*(.*)', line, re.IGNORECASE)
                if match:
                    name = match.group(1).strip()
                    # Clean up vendor prefix
                    name = re.sub(r'^(Intel|AMD|NVIDIA)\s+Corporation\s+', '', name).strip()
                    # Extract bracket name if present
                    brackets = re.findall(r'\[([^\]]+)\]', name)
                    if brackets:
                        # Use last bracket (usually the product name)
                        clean = brackets[-1]
                        # But keep vendor context
                        vendor = ""
                        if "intel" in line.lower():
                            vendor = "Intel "
                        elif "amd" in line.lower() or "ati" in line.lower():
                            vendor = "AMD "
                        elif "nvidia" in line.lower():
                            vendor = "NVIDIA "
                        name = f"{vendor}{clean}"
                    devices.append(name)
    except Exception:
        pass
    return devices


def get_desktop_info():
    """Return (desktop_env, display_server, wm)."""
    de = os.environ.get("XDG_CURRENT_DESKTOP", "")
    session_type = os.environ.get("XDG_SESSION_TYPE", "")

    # Display server
    if session_type.lower() == "wayland":
        display_server = "Wayland"
    elif session_type.lower() == "x11":
        display_server = "X11"
    elif os.environ.get("WAYLAND_DISPLAY"):
        display_server = "Wayland"
    elif os.environ.get("DISPLAY"):
        display_server = "X11"
    else:
        display_server = ""

    # Desktop environment version
    de_version = ""
    if "xfce" in de.lower():
        v = run_cmd(["xfce4-about", "--version"])
        m = re.search(r'(\d+\.\d+(?:\.\d+)?)', v)
        if m:
            de_version = m.group(1)
        de_name = f"Xfce {de_version}" if de_version else "Xfce"
    elif "gnome" in de.lower():
        v = run_cmd(["gnome-shell", "--version"])
        m = re.search(r'(\d+\.\d+(?:\.\d+)?)', v)
        if m:
            de_version = m.group(1)
        de_name = f"GNOME {de_version}" if de_version else "GNOME"
    elif "kde" in de.lower():
        de_name = "KDE Plasma"
    else:
        de_name = de if de else "Unknown"

    # Window manager
    wm = ""
    try:
        wm_out = run_cmd(["wmctrl", "-m"])
        for line in wm_out.splitlines():
            if line.startswith("Name:"):
                wm = line.split(":", 1)[1].strip()
                break
    except Exception:
        pass
    if not wm:
        # Try common WMs
        for wm_name in ["xfwm4", "mutter", "kwin", "openbox", "i3"]:
            check = run_cmd(["pgrep", "-x", wm_name])
            if check:
                wm = wm_name
                break

    return de_name, display_server, wm


def get_battery_info():
    """Return battery info if present, or None if no battery (desktop)."""
    try:
        batteries = sorted(glob.glob("/sys/class/power_supply/BAT*"))
        if not batteries:
            return None

        bat_path = Path(batteries[0])
        capacity = ""
        status = ""

        cap_file = bat_path / "capacity"
        if cap_file.exists():
            capacity = cap_file.read_text().strip() + "%"

        status_file = bat_path / "status"
        if status_file.exists():
            status = status_file.read_text().strip()

        if not capacity:
            return None

        return {"capacity": capacity, "status": status}
    except Exception:
        return None


# ── Build text summary for clipboard ────────────────────────────────

def build_summary_text():
    """Build a plain-text summary of all system info for clipboard."""
    lines = []
    version = get_ll_version()
    kernel = get_kernel()
    uptime = get_uptime()

    lines.append(f"{version}")
    lines.append(f"Kernel: {kernel}")
    if uptime:
        lines.append(f"Uptime: {uptime}")
    lines.append("")

    # Motherboard
    mb_vendor, mb_product = get_motherboard_info()
    if mb_vendor or mb_product:
        lines.append(f"Motherboard: {mb_vendor} {mb_product}".strip())

    # CPU
    cpu_model, cores, threads = get_cpu_info()
    lines.append(f"Processor: {cpu_model or 'Unknown'}")
    lines.append(f"  {cores} cores / {threads} threads")

    # Memory
    mem_total, mem_avail = get_memory_info()
    mem_used = mem_total - mem_avail
    mem_pct = (mem_used / mem_total * 100) if mem_total else 0
    lines.append(f"Memory: {format_size_kb(mem_total)} Total ({mem_pct:.0f}% used)")
    lines.append(f"  {format_size_kb(mem_used)} used / {format_size_kb(mem_avail)} available")

    # Storage
    drives = get_storage_info()
    for d in drives:
        name = d["model"] if d["model"] else d["name"]
        lines.append(f"Storage: {name} ({d['size']})")
        if d["total"] > 0:
            lines.append(f"  {format_bytes(d['used'])} used / {format_bytes(d['total'])} total ({d['fraction']*100:.0f}%)")

    # GPU
    gpus = get_gpu_info()
    for gpu in gpus:
        vram_str = f" ({gpu['vram']})" if gpu["vram"] else ""
        lines.append(f"Graphics: {gpu['name']}{vram_str}")

    # Display
    displays = get_display_info()
    for disp in displays:
        refresh_str = f" @ {disp['refresh']}" if disp["refresh"] else ""
        lines.append(f"Display: {disp['name']} {disp['resolution']}{refresh_str}")

    # Audio
    audio_devs = get_audio_info()
    for ad in audio_devs:
        lines.append(f"Audio: {ad}")

    # Desktop
    de_name, display_server, wm = get_desktop_info()
    de_parts = [de_name]
    if display_server:
        de_parts.append(display_server)
    if wm:
        de_parts.append(f"WM: {wm}")
    lines.append(f"Desktop: {' / '.join(de_parts)}")

    # Network
    ip = get_local_ip()
    hostname = get_hostname()
    lines.append(f"Network: {ip}")
    if hostname:
        lines.append(f"  Hostname: {hostname}")

    # Battery
    bat = get_battery_info()
    if bat:
        lines.append(f"Battery: {bat['capacity']} ({bat['status']})")

    return "\n".join(lines)


# ── CSS ──────────────────────────────────────────────────────────────

CSS = """
window.about-window {
    background: linear-gradient(180deg, #1a1d23 0%, #22262e 100%);
}

.header-area {
    padding: 8px 24px 6px;
    background: linear-gradient(135deg, #1e2530 0%, #2a3444 50%, #1e2530 100%);
    border-bottom: 1px solid alpha(white, 0.06);
}

.logo-image {
    margin-bottom: 2px;
}

.distro-name {
    font-size: 16px;
    font-weight: 700;
    color: #ffffff;
    letter-spacing: 0.5px;
}

.distro-tagline {
    font-size: 10px;
    color: alpha(white, 0.55);
    letter-spacing: 0.3px;
}

.info-scroll {
    background: transparent;
}

.info-card {
    background: alpha(white, 0.04);
    border-radius: 12px;
    border: 1px solid alpha(white, 0.07);
    padding: 14px 16px;
    margin: 4px 20px;
    transition: background 200ms ease;
}

.info-card:hover {
    background: alpha(white, 0.07);
}

.card-icon {
    margin-right: 12px;
}

.card-label {
    font-size: 11px;
    font-weight: 600;
    color: alpha(white, 0.4);
    letter-spacing: 1px;
}

.card-value {
    font-size: 14px;
    font-weight: 500;
    color: #e8ecf1;
}

.card-detail {
    font-size: 12px;
    color: alpha(white, 0.45);
}

.brand-badge {
    background: alpha(white, 0.06);
    border-radius: 6px;
    padding: 2px 6px;
    border: 1px solid alpha(white, 0.08);
}

.separator-line {
    background: alpha(white, 0.06);
    min-height: 1px;
    margin: 2px 24px;
}

.footer-bar {
    padding: 6px 24px;
    background: alpha(black, 0.15);
    border-top: 1px solid alpha(white, 0.05);
}

.footer-text {
    font-size: 11px;
    color: alpha(white, 0.35);
}

.copy-btn {
    font-size: 11px;
    padding: 4px 14px;
    border-radius: 6px;
    background: alpha(#3498db, 0.2);
    color: #6cb4ee;
    border: 1px solid alpha(#3498db, 0.3);
}

.copy-btn:hover {
    background: alpha(#3498db, 0.3);
}

.kernel-pill {
    background: alpha(#3498db, 0.15);
    color: #6cb4ee;
    border-radius: 20px;
    padding: 3px 12px;
    font-size: 11px;
    font-weight: 600;
    border: 1px solid alpha(#3498db, 0.2);
}

.uptime-pill {
    background: alpha(#2ecc71, 0.12);
    color: #6ddf9e;
    border-radius: 20px;
    padding: 3px 12px;
    font-size: 11px;
    font-weight: 600;
    border: 1px solid alpha(#2ecc71, 0.18);
}

.usage-bar trough {
    background: alpha(white, 0.08);
    border-radius: 4px;
    min-height: 6px;
}

.usage-bar progress {
    background: linear-gradient(90deg, #3498db, #2ecc71);
    border-radius: 4px;
    min-height: 6px;
}

.usage-bar-warn progress {
    background: linear-gradient(90deg, #e67e22, #e74c3c);
}
"""


# ── UI ───────────────────────────────────────────────────────────────

class AboutWindow(Gtk.ApplicationWindow):
    def __init__(self, app):
        super().__init__(application=app, title=APP_NAME)
        self.set_default_size(520, 720)
        self.set_resizable(False)
        self.add_css_class("about-window")

        # Load CSS
        css_provider = Gtk.CssProvider()
        css_provider.load_from_string(CSS)
        Gtk.StyleContext.add_provider_for_display(
            Gdk.Display.get_default(),
            css_provider,
            Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
        )

        # Header bar
        hb = Gtk.HeaderBar()
        hb.set_title_widget(Gtk.Label(label=APP_NAME))
        self.set_titlebar(hb)

        # Main layout
        main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        self.set_child(main_box)

        # Header area - vertical centered layout, fixed height
        header = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
        header.add_css_class("header-area")
        header.set_vexpand(False)
        header.set_valign(Gtk.Align.START)
        header.set_size_request(-1, 145)

        # Logo - use Gtk.Image (respects pixel size unlike Picture)
        logo_path = ICONS["logo"]
        if Path(logo_path).exists():
            logo = Gtk.Image.new_from_file(logo_path)
            logo.set_pixel_size(36)
            logo.set_halign(Gtk.Align.CENTER)
            logo.set_vexpand(False)
            header.append(logo)

        # Distro name
        version = get_ll_version()
        name_label = Gtk.Label(label=version)
        name_label.add_css_class("distro-name")
        name_label.set_halign(Gtk.Align.CENTER)
        name_label.set_selectable(True)
        name_label.set_focusable(False)
        header.append(name_label)

        # Kernel
        kernel = get_kernel()
        if kernel:
            kernel_label = Gtk.Label(label=f"Kernel {kernel}")
            kernel_label.add_css_class("kernel-pill")
            kernel_label.set_halign(Gtk.Align.CENTER)
            kernel_label.set_selectable(True)
            kernel_label.set_focusable(False)
            header.append(kernel_label)

        # Uptime
        uptime = get_uptime()
        if uptime:
            uptime_label = Gtk.Label(label=f"Uptime {uptime}")
            uptime_label.add_css_class("uptime-pill")
            uptime_label.set_halign(Gtk.Align.CENTER)
            header.append(uptime_label)

        main_box.append(header)

        # Scrollable info area
        scroll = Gtk.ScrolledWindow()
        scroll.set_vexpand(True)
        scroll.add_css_class("info-scroll")
        scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)

        info_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
        info_box.set_margin_top(12)
        info_box.set_margin_bottom(12)

        # ── Motherboard card ──
        mb_vendor, mb_product = get_motherboard_info()
        if mb_vendor or mb_product:
            mb_name = f"{mb_vendor} {mb_product}".strip()
            self._add_card(info_box, "motherboard", "MOTHERBOARD", mb_name, None)

        # ── CPU card (use brand icon if detected) ──
        cpu_model, cores, threads = get_cpu_info()
        cpu_brand = detect_brand(cpu_model)
        cpu_icon = cpu_brand if cpu_brand else "cpu"
        self._add_card(info_box, cpu_icon, "PROCESSOR", cpu_model or "Unknown",
                       f"{cores} cores / {threads} threads")

        # ── Memory card ──
        mem_total, mem_avail = get_memory_info()
        mem_used = mem_total - mem_avail
        mem_pct = (mem_used / mem_total * 100) if mem_total else 0
        self._add_card(info_box, "memory", "MEMORY",
                       f"{format_size_kb(mem_total)} Total",
                       f"{format_size_kb(mem_used)} used / {format_size_kb(mem_avail)} available",
                       bar_fraction=mem_pct / 100.0)

        # ── Storage cards (one per drive) ──
        drives = get_storage_info()
        for i, d in enumerate(drives):
            label = "STORAGE" if i == 0 else f"STORAGE ({i + 1})"
            name = d["model"] if d["model"] else d["name"]
            value = f"{name}  ({d['size']})"
            if d["total"] > 0:
                detail = f"{format_bytes(d['used'])} used / {format_bytes(d['total'])} total"
                self._add_card(info_box, "storage", label, value, detail,
                               bar_fraction=d["fraction"])
            else:
                self._add_card(info_box, "storage", label, value, d["name"])

        # ── GPU cards (use brand icon if detected) ──
        gpus = get_gpu_info()
        if gpus:
            for i, gpu in enumerate(gpus):
                gpu_name = gpu["name"]
                gpu_vram = gpu["vram"]
                gpu_brand = detect_brand(gpu_name)
                gpu_icon = gpu_brand if gpu_brand else "gpu"
                label = "GRAPHICS" if i == 0 else f"GRAPHICS ({i + 1})"
                detail = f"VRAM: {gpu_vram}" if gpu_vram else None
                self._add_card(info_box, gpu_icon, label, gpu_name, detail)
        else:
            self._add_card(info_box, "gpu", "GRAPHICS", "No GPU detected", None)

        # ── Display cards ──
        displays = get_display_info()
        for i, disp in enumerate(displays):
            label = "DISPLAY" if i == 0 else f"DISPLAY ({i + 1})"
            value = f"{disp['name']}  {disp['resolution']}"
            detail = f"Refresh rate: {disp['refresh']}" if disp["refresh"] else None
            self._add_card(info_box, "display", label, value, detail)

        # ── Audio cards ──
        audio_devs = get_audio_info()
        if audio_devs:
            for i, ad in enumerate(audio_devs):
                label = "AUDIO" if i == 0 else f"AUDIO ({i + 1})"
                self._add_card(info_box, "audio", label, ad, None)

        # ── Desktop Environment card ──
        de_name, display_server, wm = get_desktop_info()
        de_detail_parts = []
        if display_server:
            de_detail_parts.append(f"Display server: {display_server}")
        if wm:
            de_detail_parts.append(f"Window manager: {wm}")
        de_detail = " / ".join(de_detail_parts) if de_detail_parts else None
        self._add_card(info_box, "desktop", "DESKTOP", de_name, de_detail)

        # ── Network card ──
        ip = get_local_ip()
        hostname = get_hostname()
        self._add_card(info_box, "network", "NETWORK",
                       f"IP Address: {ip}",
                       f"Hostname: {hostname}" if hostname else None)

        # ── Battery card (only if battery present) ──
        bat = get_battery_info()
        if bat:
            self._add_card(info_box, "battery", "BATTERY",
                           f"{bat['capacity']}",
                           f"Status: {bat['status']}" if bat["status"] else None)

        scroll.set_child(info_box)
        main_box.append(scroll)

        # Footer with link and copy button
        footer = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
        footer.add_css_class("footer-bar")

        footer_link = Gtk.LinkButton(uri="https://www.linuxliteos.com", label="linuxliteos.com")
        footer_link.add_css_class("footer-text")
        footer_link.set_hexpand(True)
        footer_link.set_halign(Gtk.Align.START)
        footer.append(footer_link)

        copy_btn = Gtk.Button(label="Copy All to Clipboard")
        copy_btn.add_css_class("copy-btn")
        copy_btn.set_halign(Gtk.Align.END)
        copy_btn.connect("clicked", self._on_copy_clicked)
        footer.append(copy_btn)

        main_box.append(footer)

    def _on_copy_clicked(self, btn):
        text = build_summary_text()
        clipboard = Gdk.Display.get_default().get_clipboard()
        clipboard.set(text)
        # Brief feedback
        btn.set_label("Copied!")
        GLib.timeout_add(1500, lambda: btn.set_label("Copy All to Clipboard") or False)

    def _add_card(self, parent, icon_key, label, value, detail=None, bar_fraction=None):
        card = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0)
        card.add_css_class("info-card")

        # Icon (40x40)
        icon_path = ICONS.get(icon_key, "")
        if icon_path and Path(icon_path).exists():
            icon = Gtk.Picture.new_for_filename(icon_path)
            icon.set_size_request(40, 40)
            icon.set_can_shrink(True)
            icon.set_content_fit(Gtk.ContentFit.SCALE_DOWN)
            icon.add_css_class("card-icon")
            icon.set_valign(Gtk.Align.CENTER)
            card.append(icon)

        # Text content
        text_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
        text_box.set_hexpand(True)
        text_box.set_valign(Gtk.Align.CENTER)

        lbl = Gtk.Label(label=label)
        lbl.add_css_class("card-label")
        lbl.set_xalign(0)
        lbl.set_selectable(True)
        lbl.set_focusable(False)
        text_box.append(lbl)

        val = Gtk.Label(label=value)
        val.add_css_class("card-value")
        val.set_xalign(0)
        val.set_selectable(True)
        val.set_focusable(False)
        val.set_wrap(True)
        val.set_wrap_mode(Pango.WrapMode.WORD_CHAR)
        text_box.append(val)

        if detail:
            det = Gtk.Label(label=detail)
            det.add_css_class("card-detail")
            det.set_xalign(0)
            det.set_selectable(True)
            det.set_focusable(False)
            det.set_wrap(True)
            det.set_wrap_mode(Pango.WrapMode.WORD_CHAR)
            text_box.append(det)

        if bar_fraction is not None:
            bar = Gtk.ProgressBar()
            bar.set_fraction(bar_fraction)
            bar.add_css_class("usage-bar")
            if bar_fraction > 0.85:
                bar.add_css_class("usage-bar-warn")
            bar.set_margin_top(6)
            text_box.append(bar)

        card.append(text_box)
        parent.append(card)


class LiteAboutApp(Gtk.Application):
    def __init__(self):
        super().__init__(application_id=APP_ID, flags=Gio.ApplicationFlags.FLAGS_NONE)

    def do_activate(self):
        win = AboutWindow(self)
        win.present()

    def do_startup(self):
        Gtk.Application.do_startup(self)
        GLib.set_application_name(APP_NAME)
        try:
            display = Gdk.Display.get_default()
            theme = Gtk.IconTheme.get_for_display(display)
            theme.add_search_path(str(Path(ICONS["logo"]).parent))
            self.set_icon_name("lite-about")
        except Exception:
            pass


def main():
    app = LiteAboutApp()
    return app.run(sys.argv)


if __name__ == "__main__":
    sys.exit(main())
