#!/usr/bin/env python3
import json
import math
import os
import platform
import re
import shutil
import socket
import select
import subprocess
import sys
import threading
import time
from datetime import datetime
from pathlib import Path

try:
    import gi
    gi.require_version("Gtk", "3.0")
    from gi.repository import Gtk, Gdk, GLib, Pango
except Exception as exc:
    print("Govibe PC Specs needs python3-gi and gir1.2-gtk-3.0 installed.", file=sys.stderr)
    print(str(exc), file=sys.stderr)
    sys.exit(1)

APP_NAME = "Govibe PC Specs"
APP_ID = "org.govibe.pcspecs"
VERSION = "1.0.4"
MISSING = "Not detected"
GVHWMEM_HELPER = "/usr/lib/govibepcspecs/gvhwmem"
GVHWGPU_HELPER = "/usr/lib/govibepcspecs/gvhwgpu"


def read_text(path, default=""):
    try:
        return Path(path).read_text(errors="ignore").strip()
    except Exception:
        return default


def run_cmd(cmd, timeout=3):
    try:
        p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True, timeout=timeout)
        return p.stdout.strip()
    except Exception:
        return ""


def run_shell(cmd, timeout=3):
    try:
        p = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True, timeout=timeout)
        return p.stdout.strip()
    except Exception:
        return ""


def parse_key_value(text, sep=":"):
    data = {}
    for line in text.splitlines():
        if sep in line:
            k, v = line.split(sep, 1)
            data[k.strip()] = v.strip()
    return data


def first_nonempty(*values):
    for v in values:
        if v is None:
            continue
        s = str(v).strip()
        if s:
            return s
    return MISSING


def bytes_human(num):
    try:
        num = float(num)
    except Exception:
        return str(num)
    units = ["B", "KB", "MB", "GB", "TB", "PB"]
    i = 0
    while num >= 1024 and i < len(units) - 1:
        num /= 1024.0
        i += 1
    if i == 0:
        return f"{int(num)} {units[i]}"
    return f"{num:.2f} {units[i]}"


def kb_human(kb):
    try:
        return bytes_human(int(kb) * 1024)
    except Exception:
        return str(kb)


def unique_clean(values):
    clean = []
    seen = set()
    for value in values:
        text = str(value or "").strip()
        if not text or text.lower() in {"unknown", "not specified", "not provided", "none", "0", "0 mv"}:
            continue
        key = text.lower()
        if key not in seen:
            seen.add(key)
            clean.append(text)
    return clean


def join_unique(values, missing=MISSING):
    clean = unique_clean(values)
    return " | ".join(clean) if clean else missing


def run_system_cmd(cmd, timeout=8):
    """Run a normal command, then try non-interactive sudo if the user has cached sudo rights."""
    out = run_cmd(cmd, timeout=timeout)
    if out:
        return out
    if os.geteuid() != 0 and shutil.which("sudo"):
        out = run_cmd(["sudo", "-n"] + list(cmd), timeout=timeout)
        if out:
            return out
    return ""


def sysfs_pci_path(bus_id):
    if not bus_id:
        return None
    candidates = [
        Path("/sys/bus/pci/devices") / bus_id,
        Path("/sys/bus/pci/devices") / ("0000:" + bus_id if not bus_id.startswith("0000:") else bus_id),
    ]
    for candidate in candidates:
        if candidate.exists():
            return candidate
    return None


def read_first(paths, default=""):
    for path in paths:
        value = read_text(path)
        if value:
            return value
    return default


def active_dpm_value(text):
    for line in text.splitlines():
        if "*" in line:
            value = line.replace("*", "").strip()
            if ":" in value:
                value = value.split(":", 1)[1].strip()
            return value.replace("Mhz", "MHz").replace("Ghz", "GHz")
    return ""


def parse_lspci_link(bus_id):
    if not bus_id or not shutil.which("lspci"):
        return {}
    out = run_cmd(["lspci", "-vv", "-s", bus_id], timeout=4)
    data = {}
    for line in out.splitlines():
        clean = line.strip()
        if clean.startswith("LnkCap:"):
            speed = re.search(r"Speed\s+([^,]+)", clean)
            width = re.search(r"Width\s+(x\d+)", clean)
            if speed:
                data["PCIe max speed"] = speed.group(1).strip()
            if width:
                data["PCIe max lanes"] = width.group(1).strip()
        elif clean.startswith("LnkSta:"):
            speed = re.search(r"Speed\s+([^,]+)", clean)
            width = re.search(r"Width\s+(x\d+)", clean)
            if speed:
                data["PCIe current speed"] = speed.group(1).strip()
            if width:
                data["PCIe current lanes"] = width.group(1).strip()
    return data


def translate_memory_vendor(value):
    text = str(value or "").strip()
    if not text:
        return ""
    low = text.lower()
    known = {
        "samsung": "Samsung",
        "hynix": "SK Hynix",
        "sk hynix": "SK Hynix",
        "micron": "Micron",
        "elpida": "Elpida",
        "nanya": "Nanya",
        "qimonda": "Qimonda",
        "etron": "Etron",
    }
    for needle, name in known.items():
        if needle in low:
            return name
    return text


def parse_dmidecode_memory(text):
    devices = []
    current = None
    in_memory_device = False
    for line in text.splitlines():
        raw = line.rstrip()
        clean = raw.strip()
        if clean == "Memory Device":
            if current:
                devices.append(current)
            current = {}
            in_memory_device = True
            continue
        if clean.startswith("Handle ") and in_memory_device and current:
            devices.append(current)
            current = None
            in_memory_device = False
            continue
        if in_memory_device and current is not None and ":" in clean:
            key, value = clean.split(":", 1)
            current[key.strip()] = value.strip()
    if current:
        devices.append(current)
    return devices


def collect_smbios_memory_devices():
    """Use the bundled Govibe C SMBIOS reader for RAM slot details."""
    helper = Path(GVHWMEM_HELPER)
    if helper.exists() and os.access(str(helper), os.X_OK):
        out = run_cmd([str(helper)], timeout=5)
        devices = parse_dmidecode_memory(out) if out else []
        if devices:
            return devices, "Govibe C SMBIOS reader"
    return [], ""



def probe_gpu_paths(pci_path):
    paths = []
    if pci_path:
        paths.append(pci_path)
        drm_dir = pci_path / "drm"
        if drm_dir.exists():
            paths.extend(sorted(drm_dir.glob("card*")))
    return paths


def normalize_pci_id(value):
    text = str(value or "").strip().lower()
    if not text:
        return ""
    if text.startswith("0000:"):
        return text
    if re.match(r"^[0-9a-f]{2}:[0-9a-f]{2}\.\d$", text):
        return "0000:" + text
    return text


def detect_memory_brand_from_text(value):
    text = str(value or "")
    low = text.lower()
    if not low:
        return ""
    direct = [
        ("samsung", "Samsung"),
        ("sk hynix", "SK Hynix"),
        ("hynix", "SK Hynix"),
        ("hyundai", "SK Hynix"),
        ("micron", "Micron"),
        ("elpida", "Elpida"),
        ("nanya", "Nanya"),
        ("qimonda", "Qimonda"),
        ("etron", "Etron"),
    ]
    for needle, name in direct:
        if needle in low:
            return name
    upper = text.upper()
    part_patterns = [
        (r"\bK4[AGZ][A-Z0-9]{3,}\b", "Samsung"),
        (r"\bH5G[A-Z0-9]{3,}\b", "SK Hynix"),
        (r"\bH5GC[A-Z0-9]{2,}\b", "SK Hynix"),
        (r"\bMT5[13][A-Z0-9]{3,}\b", "Micron"),
        (r"\bMT6[01][A-Z0-9]{3,}\b", "Micron"),
        (r"\bEDW[A-Z0-9]{4,}\b", "Elpida"),
        (r"\bNT5[A-Z0-9]{4,}\b", "Nanya"),
    ]
    for pattern, name in part_patterns:
        if re.search(pattern, upper):
            return name
    return ""


def collect_rocm_memory_vendors():
    if not shutil.which("rocm-smi"):
        return []
    out = run_cmd(["rocm-smi", "--showmemvendor"], timeout=6)
    vendors = []
    for line in out.splitlines():
        if re.search(r"(VRAM|memory).*vendor", line, re.I):
            value = line.split(":")[-1].strip()
            brand = detect_memory_brand_from_text(value) or translate_memory_vendor(value)
            if brand and brand not in vendors:
                vendors.append(brand)
    return vendors



def parse_gpu_c_detector(text):
    devices = []
    current = None
    for line in text.splitlines():
        clean = line.strip()
        if clean == "GPU Hardware Device":
            if current:
                devices.append(current)
            current = {}
            continue
        if current is not None and ":" in clean:
            key, value = clean.split(":", 1)
            current[key.strip()] = value.strip()
    if current:
        devices.append(current)
    return devices


def c_gpu_memory_brand_for_bus(bus_id=""):
    """Use the bundled Govibe C GPU detector for VRAM chip brand probing."""
    helper = Path(GVHWGPU_HELPER)
    if not (helper.exists() and os.access(str(helper), os.X_OK)):
        return ""
    out = run_cmd([str(helper)], timeout=10)
    if not out:
        return ""
    devices = parse_gpu_c_detector(out)
    wanted = normalize_pci_id(bus_id)
    fallback = ""
    for dev in devices:
        pci = normalize_pci_id(dev.get("PCI Address", ""))
        brand = dev.get("Memory Chip Brand", "").strip()
        if not brand:
            continue
        if wanted and pci == wanted:
            return brand
        if not fallback:
            fallback = brand
    return fallback


def nvidia_gpu_memory_vendor_for_bus(bus_id=""):
    if not shutil.which("nvidia-smi"):
        return ""
    help_text = run_cmd(["nvidia-smi", "--help-query-gpu"], timeout=4)
    supported = []
    for field in ["memory.vendor", "memory.manufacturer", "memory.type", "ram_type"]:
        if field in help_text:
            supported.append(field)
    if not supported:
        return ""
    query = "pci.bus_id," + ",".join(supported)
    out = run_cmd(["nvidia-smi", f"--query-gpu={query}", "--format=csv,noheader"], timeout=5)
    wanted = normalize_pci_id(bus_id)
    fallback = ""
    for line in out.splitlines():
        parts = [x.strip() for x in line.split(",")]
        if len(parts) < 2:
            continue
        pci = normalize_pci_id(parts[0])
        values = [x for x in parts[1:] if x and x.lower() not in {"n/a", "[not supported]", "unknown"}]
        brand = detect_memory_brand_from_text(" ".join(values)) or join_unique(values, missing="")
        if not brand:
            continue
        if wanted and pci == wanted:
            return brand
        if not fallback:
            fallback = brand
    return fallback


def read_gpu_rom_ascii(pci_path):
    if not pci_path or os.geteuid() != 0:
        return ""
    rom = pci_path / "rom"
    if not rom.exists():
        return ""
    data = b""
    enabled = False
    try:
        try:
            rom.write_text("1")
            enabled = True
        except Exception:
            enabled = False
        if enabled:
            data = rom.read_bytes()[:1048576]
    except Exception:
        data = b""
    finally:
        if enabled:
            try:
                rom.write_text("0")
            except Exception:
                pass
    if not data:
        return ""
    text = re.sub(r"[^\x20-\x7e]+", " ", data.decode("latin1", errors="ignore"))
    return text


def gpu_memory_chip_brand(pci_path, bus_id="", device_text="", driver=""):
    values = []
    for base in probe_gpu_paths(pci_path):
        for name in [
            "mem_info_vram_vendor",
            "vram_vendor",
            "memory_vendor",
            "mem_vendor",
            "mem_info_vram_type",
            "vram_type",
            "memory_type",
        ]:
            values.append(read_text(base / name))
            values.append(read_text(base / "device" / name))
    for value in values:
        brand = detect_memory_brand_from_text(value) or translate_memory_vendor(value)
        if brand:
            return brand

    cbrand = c_gpu_memory_brand_for_bus(bus_id)
    if cbrand:
        return cbrand

    nv = nvidia_gpu_memory_vendor_for_bus(bus_id)
    if nv:
        return nv

    rocm_vendors = collect_rocm_memory_vendors()
    if rocm_vendors:
        return " | ".join(rocm_vendors)

    rom_brand = detect_memory_brand_from_text(read_gpu_rom_ascii(pci_path))
    if rom_brand:
        return rom_brand

    driver_l = str(driver or "").lower()
    dev_l = str(device_text or "").lower()
    if any(x in driver_l for x in ["i915", "xe"]) or "intel" in dev_l:
        return "Shared system memory"
    return "Unknown"

def collect_drm_gpu_details(pci_path):
    rows = []
    if not pci_path:
        return rows
    probe_paths = [pci_path]
    drm_dir = pci_path / "drm"
    if drm_dir.exists():
        probe_paths.extend(sorted(drm_dir.glob("card*")))
    files = []
    for base in probe_paths:
        files.extend([
            base / "device" / "mem_info_vram_total",
            base / "mem_info_vram_total",
        ])
    vram_total = read_first(files)
    vram_used = read_first([p / "device" / "mem_info_vram_used" for p in probe_paths] + [p / "mem_info_vram_used" for p in probe_paths])
    vis_total = read_first([p / "device" / "mem_info_vis_vram_total" for p in probe_paths] + [p / "mem_info_vis_vram_total" for p in probe_paths])
    gtt_total = read_first([p / "device" / "mem_info_gtt_total" for p in probe_paths] + [p / "mem_info_gtt_total" for p in probe_paths])
    if vram_total:
        rows.append(("GPU memory", bytes_human(vram_total)))
    if vram_used:
        rows.append(("GPU memory used", bytes_human(vram_used)))
    if vis_total:
        rows.append(("Visible GPU memory", bytes_human(vis_total)))
    if gtt_total:
        rows.append(("GTT/shared memory", bytes_human(gtt_total)))

    cur_link_speed = read_first([pci_path / "current_link_speed"])
    cur_link_width = read_first([pci_path / "current_link_width"])
    max_link_speed = read_first([pci_path / "max_link_speed"])
    max_link_width = read_first([pci_path / "max_link_width"])
    if cur_link_speed or cur_link_width:
        rows.append(("PCIe current link", " | ".join([x for x in [cur_link_speed, f"x{cur_link_width}" if cur_link_width else ""] if x])))
    if max_link_speed or max_link_width:
        rows.append(("PCIe max link", " | ".join([x for x in [max_link_speed, f"x{max_link_width}" if max_link_width else ""] if x])))

    vendor = read_first([
        pci_path / "vram_vendor",
        pci_path / "mem_info_vram_vendor",
        pci_path / "vram_type",
        pci_path / "mem_info_vram_type",
    ])
    if vendor:
        rows.append(("GPU memory chip brand", detect_memory_brand_from_text(vendor) or translate_memory_vendor(vendor)))

    mclk = active_dpm_value(read_first([pci_path / "pp_dpm_mclk"]))
    sclk = active_dpm_value(read_first([pci_path / "pp_dpm_sclk"]))
    pcie_state = active_dpm_value(read_first([pci_path / "pp_dpm_pcie"]))
    if mclk:
        rows.append(("GPU memory frequency", mclk))
    if sclk:
        rows.append(("GPU core frequency", sclk))
    if pcie_state:
        rows.append(("PCIe active state", pcie_state))

    gpu_busy = read_first([pci_path / "gpu_busy_percent"])
    mem_busy = read_first([pci_path / "mem_busy_percent"])
    if gpu_busy:
        rows.append(("GPU busy", f"{gpu_busy}%"))
    if mem_busy:
        rows.append(("GPU memory busy", f"{mem_busy}%"))

    voltage = read_first([pci_path / "pp_od_clk_voltage"])
    if voltage:
        short = " | ".join([line.strip() for line in voltage.splitlines() if line.strip()][:6])
        if short:
            rows.append(("GPU voltage table", short))
    return rows


def collect_nvidia_smi_details():
    rows = []
    if not shutil.which("nvidia-smi"):
        return rows
    query = "name,memory.total,memory.used,memory.free,clocks.mem,clocks.gr,pcie.link.gen.current,pcie.link.gen.max,pcie.link.width.current,pcie.link.width.max,driver_version,vbios_version"
    out = run_cmd(["nvidia-smi", f"--query-gpu={query}", "--format=csv,noheader,nounits"], timeout=5)
    fields = [f.strip() for f in query.split(",")]
    for index, line in enumerate([x for x in out.splitlines() if x.strip()], 1):
        parts = [p.strip() for p in line.split(",")]
        data = dict(zip(fields, parts))
        prefix = f"NVIDIA GPU {index}"
        rows.append((f"{prefix} name", data.get("name", MISSING)))
        if data.get("memory.total"):
            rows.append((f"{prefix} memory", f"{data.get('memory.total')} MB"))
        if data.get("memory.used"):
            rows.append((f"{prefix} memory used", f"{data.get('memory.used')} MB"))
        if data.get("memory.free"):
            rows.append((f"{prefix} memory free", f"{data.get('memory.free')} MB"))
        if data.get("clocks.mem"):
            rows.append((f"{prefix} memory frequency", f"{data.get('clocks.mem')} MHz"))
        if data.get("clocks.gr"):
            rows.append((f"{prefix} core frequency", f"{data.get('clocks.gr')} MHz"))
        cur = " | ".join([x for x in [f"Gen {data.get('pcie.link.gen.current')}" if data.get("pcie.link.gen.current") else "", f"x{data.get('pcie.link.width.current')}" if data.get("pcie.link.width.current") else ""] if x])
        mx = " | ".join([x for x in [f"Gen {data.get('pcie.link.gen.max')}" if data.get("pcie.link.gen.max") else "", f"x{data.get('pcie.link.width.max')}" if data.get("pcie.link.width.max") else ""] if x])
        if cur:
            rows.append((f"{prefix} PCIe current link", cur))
        if mx:
            rows.append((f"{prefix} PCIe max link", mx))
        if data.get("driver_version"):
            rows.append((f"{prefix} driver", data.get("driver_version")))
        if data.get("vbios_version"):
            rows.append((f"{prefix} VBIOS", data.get("vbios_version")))
    return rows


def get_meminfo():
    data = {}
    for line in read_text("/proc/meminfo").splitlines():
        if ":" in line:
            k, v = line.split(":", 1)
            m = re.search(r"(\d+)", v)
            if m:
                data[k.strip()] = int(m.group(1))
    return data


def get_lscpu():
    return parse_key_value(run_cmd(["lscpu"], timeout=3))


def get_cpuinfo():
    blocks = read_text("/proc/cpuinfo").split("\n\n")
    cpus = []
    for block in blocks:
        d = parse_key_value(block)
        if d:
            cpus.append(d)
    return cpus


def read_cpufreq_value(name):
    paths = [
        f"/sys/devices/system/cpu/cpu0/cpufreq/{name}",
        f"/sys/devices/system/cpu/cpufreq/policy0/{name}",
    ]
    for path in paths:
        v = read_text(path)
        if v:
            return v
    return ""


def khz_to_mhz(value):
    try:
        return f"{int(value) / 1000:.0f} MHz"
    except Exception:
        return value or MISSING


def collect_cpu():
    lscpu = get_lscpu()
    cpus = get_cpuinfo()
    first = cpus[0] if cpus else {}
    cur = read_cpufreq_value("scaling_cur_freq") or first.get("cpu MHz", "")
    if cur and cur.isdigit():
        cur = khz_to_mhz(cur)
    elif cur:
        try:
            cur = f"{float(cur):.0f} MHz"
        except Exception:
            pass
    minf = read_cpufreq_value("cpuinfo_min_freq")
    maxf = read_cpufreq_value("cpuinfo_max_freq")
    gov = read_cpufreq_value("scaling_governor")
    flags = first.get("flags", first.get("Features", ""))
    rows = [
        ("Model", first_nonempty(lscpu.get("Model name"), first.get("model name"), platform.processor())),
        ("Vendor", first_nonempty(first.get("vendor_id"), lscpu.get("Vendor ID"))),
        ("Architecture", first_nonempty(lscpu.get("Architecture"), platform.machine())),
        ("CPU cores", first_nonempty(lscpu.get("CPU(s)"), os.cpu_count())),
        ("Threads per core", first_nonempty(lscpu.get("Thread(s) per core"))),
        ("Cores per socket", first_nonempty(lscpu.get("Core(s) per socket"))),
        ("Sockets", first_nonempty(lscpu.get("Socket(s)"))),
        ("Current speed", first_nonempty(cur)),
        ("Minimum speed", khz_to_mhz(minf) if minf else MISSING),
        ("Maximum speed", khz_to_mhz(maxf) if maxf else MISSING),
        ("Governor", first_nonempty(gov)),
        ("Cache", first_nonempty(first.get("cache size"), lscpu.get("L3 cache"))),
        ("Virtualization", first_nonempty(lscpu.get("Virtualization"))),
        ("Byte order", first_nonempty(lscpu.get("Byte Order"))),
        ("Kernel", platform.release()),
    ]
    caches = []
    for key in ["L1d cache", "L1i cache", "L2 cache", "L3 cache"]:
        if lscpu.get(key):
            caches.append((key, lscpu.get(key)))
    feature_rows = []
    if flags:
        important = ["sse", "sse2", "sse3", "ssse3", "sse4_1", "sse4_2", "avx", "avx2", "avx512f", "aes", "fma", "vmx", "svm"]
        flags_set = set(flags.split())
        for f in important:
            feature_rows.append((f.upper(), "Yes" if f in flags_set else "No"))
    return {"main": rows, "caches": caches, "features": feature_rows, "raw_flags": flags}


def collect_memory():
    mem = get_meminfo()
    total = mem.get("MemTotal", 0)
    avail = mem.get("MemAvailable", 0)
    used = max(total - avail, 0) if total else 0
    swap_total = mem.get("SwapTotal", 0)
    swap_free = mem.get("SwapFree", 0)
    rows = [
        ("Total", kb_human(total) if total else MISSING),
        ("Available", kb_human(avail) if avail else MISSING),
        ("Used", kb_human(used) if total else MISSING),
        ("Usage", f"{(used / total * 100):.1f}%" if total else MISSING),
        ("Swap total", kb_human(swap_total) if swap_total else "None"),
        ("Swap used", kb_human(swap_total - swap_free) if swap_total else "None"),
        ("Cached", kb_human(mem.get("Cached", 0)) if mem.get("Cached") else MISSING),
        ("Buffers", kb_human(mem.get("Buffers", 0)) if mem.get("Buffers") else MISSING),
        ("Huge pages total", str(mem.get("HugePages_Total", 0))),
    ]

    dmi_rows = []
    devices, memory_source = collect_smbios_memory_devices()

    installed = []
    for dev in devices:
        size = dev.get("Size", "")
        if size and "No Module" not in size and "No Module Installed" not in size:
            installed.append(dev)

    if installed:
        rows.extend([
            ("Installed modules", str(len(installed))),
            ("Memory type", join_unique([d.get("Type") for d in installed])),
            ("Memory frequency", join_unique([d.get("Speed") for d in installed])),
            ("Configured frequency", join_unique([d.get("Configured Memory Speed") for d in installed])),
            ("Configured voltage", join_unique([d.get("Configured Voltage") for d in installed])),
            ("Minimum voltage", join_unique([d.get("Minimum Voltage") for d in installed])),
            ("Maximum voltage", join_unique([d.get("Maximum Voltage") for d in installed])),
            ("Memory manufacturer", join_unique([d.get("Manufacturer") for d in installed])),
            ("Memory part number", join_unique([d.get("Part Number") for d in installed])),
        ])
        for i, d in enumerate(installed, 1):
            slot = first_nonempty(d.get("Locator"), d.get("Bank Locator"), f"Slot {i}")
            summary = " | ".join([x for x in [
                d.get("Size"),
                d.get("Type"),
                f"Speed {d.get('Speed')}" if d.get("Speed") else "",
                f"Configured {d.get('Configured Memory Speed')}" if d.get("Configured Memory Speed") else "",
                translate_memory_vendor(d.get("Manufacturer")),
                d.get("Part Number"),
            ] if x])
            dmi_rows.append((slot, summary or MISSING))
            voltage = " | ".join([x for x in [
                f"Configured {d.get('Configured Voltage')}" if d.get("Configured Voltage") else "",
                f"Min {d.get('Minimum Voltage')}" if d.get("Minimum Voltage") else "",
                f"Max {d.get('Maximum Voltage')}" if d.get("Maximum Voltage") else "",
            ] if x])
            if voltage:
                dmi_rows.append((f"{slot} voltage", voltage))
            details = " | ".join([x for x in [
                f"Bank {d.get('Bank Locator')}" if d.get("Bank Locator") else "",
                f"Form {d.get('Form Factor')}" if d.get("Form Factor") else "",
                f"Rank {d.get('Rank')}" if d.get("Rank") else "",
                d.get("Type Detail"),
                f"Serial {d.get('Serial Number')}" if d.get("Serial Number") else "",
            ] if x])
            if details:
                dmi_rows.append((f"{slot} details", details))
    else:
        dmi_rows.append(("SMBIOS memory", "No RAM slot table was exposed by this machine firmware."))
    if memory_source:
        dmi_rows.insert(0, ("Hardware reader", memory_source))
    return {"main": rows, "modules": dmi_rows}

def collect_motherboard():
    base = "/sys/class/dmi/id"
    rows = [
        ("System vendor", first_nonempty(read_text(f"{base}/sys_vendor"))),
        ("Product name", first_nonempty(read_text(f"{base}/product_name"))),
        ("Product version", first_nonempty(read_text(f"{base}/product_version"))),
        ("Board vendor", first_nonempty(read_text(f"{base}/board_vendor"))),
        ("Board name", first_nonempty(read_text(f"{base}/board_name"))),
        ("Board version", first_nonempty(read_text(f"{base}/board_version"))),
        ("BIOS vendor", first_nonempty(read_text(f"{base}/bios_vendor"))),
        ("BIOS version", first_nonempty(read_text(f"{base}/bios_version"))),
        ("BIOS date", first_nonempty(read_text(f"{base}/bios_date"))),
        ("Chassis type", first_nonempty(read_text(f"{base}/chassis_type"))),
        ("OS", first_nonempty(run_cmd(["lsb_release", "-ds"], timeout=2).strip('"'), platform.platform())),
        ("Hostname", socket.gethostname()),
    ]
    return {"main": rows}


def collect_graphics():
    lspci = run_cmd(["lspci", "-D", "-nnk"], timeout=4) or run_cmd(["lspci", "-nnk"], timeout=4)
    devices = []
    current = None
    for line in lspci.splitlines():
        if re.search(r"(VGA compatible controller|3D controller|Display controller)", line, re.I):
            if current:
                devices.append(current)
            parts = line.split(None, 1)
            bus_id = parts[0] if parts else ""
            device_text = parts[1] if len(parts) > 1 else line.strip()
            current = {"Bus": bus_id, "Device": device_text.strip()}
        elif current and line.startswith("\t"):
            s = line.strip()
            if ":" in s:
                k, v = s.split(":", 1)
                current[k.strip()] = v.strip()
    if current:
        devices.append(current)

    rows = []
    for i, d in enumerate(devices, 1):
        bus_id = d.get("Bus", "")
        pci_path = sysfs_pci_path(bus_id)
        rows.append((f"GPU {i}", d.get("Device", MISSING)))
        if bus_id:
            rows.append((f"GPU {i} PCI address", bus_id))
        rows.append((f"GPU {i} driver", d.get("Kernel driver in use", MISSING)))
        mem_brand = gpu_memory_chip_brand(pci_path, bus_id, d.get("Device", ""), d.get("Kernel driver in use", ""))
        if mem_brand:
            rows.append((f"GPU {i} memory chip brand", mem_brand))
        if d.get("Kernel modules"):
            rows.append((f"GPU {i} modules", d.get("Kernel modules")))
        link = parse_lspci_link(bus_id)
        if link.get("PCIe current speed") or link.get("PCIe current lanes"):
            rows.append((f"GPU {i} PCIe current", " | ".join([x for x in [link.get("PCIe current speed"), link.get("PCIe current lanes")] if x])))
        if link.get("PCIe max speed") or link.get("PCIe max lanes"):
            rows.append((f"GPU {i} PCIe max", " | ".join([x for x in [link.get("PCIe max speed"), link.get("PCIe max lanes")] if x])))
        for key, value in collect_drm_gpu_details(pci_path):
            rows.append((f"GPU {i} {key}", value))

    rows.extend(collect_nvidia_smi_details())

    glx = run_cmd(["glxinfo", "-B"], timeout=4) if shutil.which("glxinfo") else ""
    glx_data = parse_key_value(glx)
    for key in ["OpenGL vendor string", "OpenGL renderer string", "OpenGL core profile version string", "OpenGL version string", "Video memory", "Unified memory", "Accelerated"]:
        if glx_data.get(key):
            label = key.replace(" string", "")
            if key == "Video memory":
                label = "OpenGL reported GPU memory"
            rows.append((label, glx_data.get(key)))

    vk = run_cmd(["vulkaninfo", "--summary"], timeout=5) if shutil.which("vulkaninfo") else ""
    if vk:
        for line in vk.splitlines():
            if any(x in line for x in ["deviceName", "driverName", "apiVersion"]):
                if "=" in line:
                    k, v = line.split("=", 1)
                    rows.append((k.strip(), v.strip()))

    xrandr = run_cmd(["xrandr", "--current"], timeout=3) if shutil.which("xrandr") else ""
    displays = []
    for line in xrandr.splitlines():
        if " connected" in line:
            displays.append(line.strip())
    if displays:
        rows.append(("Displays", " | ".join(displays)))
    if not rows:
        rows.append(("Graphics", "Install pciutils or run on a normal Linux desktop to detect GPU details"))
    return {"main": rows, "raw": lspci}

def collect_storage():
    rows = []
    out = run_cmd(["lsblk", "-J", "-o", "NAME,MODEL,SIZE,TYPE,ROTA,TRAN,MOUNTPOINT,FSTYPE,FSUSED,FSUSE%,SERIAL,STATE"], timeout=4)
    try:
        data = json.loads(out)
    except Exception:
        data = {"blockdevices": []}

    def walk(dev, prefix=""):
        name = dev.get("name", "")
        typ = dev.get("type", "")
        model = (dev.get("model") or "").strip()
        size = dev.get("size") or ""
        tran = dev.get("tran") or ""
        rota = dev.get("rota")
        mount = dev.get("mountpoint") or ""
        fs = dev.get("fstype") or ""
        used = dev.get("fsused") or ""
        usep = dev.get("fsuse%") or ""
        label = f"{prefix}{name}"
        kind = "SSD/NVMe" if rota == False else "HDD" if rota == True else typ.upper()
        value = " | ".join([x for x in [kind, typ, model, size, tran, fs, mount, used, usep] if str(x)])
        rows.append((label, value or MISSING))
        for child in dev.get("children") or []:
            walk(child, prefix + "  ")

    for dev in data.get("blockdevices", []):
        walk(dev)
    if not rows:
        rows.append(("Storage", "No block device details found"))
    return {"main": rows}


def collect_network():
    rows = []
    ip = run_cmd(["ip", "-brief", "addr"], timeout=3)
    if ip:
        for line in ip.splitlines():
            parts = line.split()
            if len(parts) >= 2:
                iface = parts[0]
                state = parts[1]
                addrs = " ".join(parts[2:]) if len(parts) > 2 else ""
                speed = read_text(f"/sys/class/net/{iface}/speed")
                duplex = read_text(f"/sys/class/net/{iface}/duplex")
                mac = read_text(f"/sys/class/net/{iface}/address")
                value = " | ".join([x for x in [state, addrs, f"{speed} Mb/s" if speed and speed != "-1" else "", duplex, mac] if x])
                rows.append((iface, value or state))
    if not rows:
        rows.append(("Network", "No network details found"))
    route = run_cmd(["ip", "route"], timeout=3)
    for line in route.splitlines()[:4]:
        rows.append(("Route", line))
    return {"main": rows}


def collect_sensors():
    rows = []
    if shutil.which("sensors"):
        out = run_cmd(["sensors"], timeout=4)
        chip = "Sensors"
        for line in out.splitlines():
            if not line.strip():
                continue
            if not line.startswith(" ") and not line.startswith("\t") and ":" not in line:
                chip = line.strip()
                continue
            if ":" in line:
                k, v = line.split(":", 1)
                rows.append((f"{chip} {k.strip()}", re.sub(r"\s+", " ", v.strip())))
    for zone in sorted(Path("/sys/class/thermal").glob("thermal_zone*")):
        temp = read_text(zone / "temp")
        typ = read_text(zone / "type")
        try:
            c = int(temp) / 1000.0
            rows.append((typ or zone.name, f"{c:.1f} °C"))
        except Exception:
            pass
    for hw in sorted(Path("/sys/class/hwmon").glob("hwmon*")):
        name = read_text(hw / "name") or hw.name
        for temp_file in sorted(hw.glob("temp*_input")):
            idx = temp_file.name.replace("temp", "").replace("_input", "")
            label = read_text(hw / f"temp{idx}_label") or f"temp{idx}"
            temp = read_text(temp_file)
            try:
                c = int(temp) / 1000.0
                rows.append((f"{name} {label}", f"{c:.1f} °C"))
            except Exception:
                pass
    seen = set()
    clean = []
    for k, v in rows:
        key = (k, v)
        if key not in seen:
            seen.add(key)
            clean.append((k, v))
    if not clean:
        clean.append(("Sensors", "Install lm-sensors and run sudo sensors-detect for more readings"))
    return {"main": clean[:80]}


def collect_overview(cpu, mem, board, gpu):
    rows = []
    cpu_model = next((v for k, v in cpu["main"] if k == "Model"), MISSING)
    mem_total = next((v for k, v in mem["main"] if k == "Total"), MISSING)
    board_name = next((v for k, v in board["main"] if k == "Board name"), MISSING)
    board_vendor = next((v for k, v in board["main"] if k == "Board vendor"), "")
    gpu_name = next((v for k, v in gpu["main"] if k.startswith("GPU 1")), MISSING)
    os_name = next((v for k, v in board["main"] if k == "OS"), MISSING)
    rows.extend([
        ("Computer", socket.gethostname()),
        ("Operating system", os_name),
        ("CPU", cpu_model),
        ("Memory", mem_total),
        ("Motherboard", first_nonempty(" ".join([board_vendor, board_name]).strip())),
        ("Graphics", gpu_name),
        ("Kernel", platform.release()),
        ("Machine", platform.machine()),
        ("Report time", datetime.now().strftime("%Y-%m-%d %H:%M:%S")),
    ])
    return {"main": rows}


def collect_all():
    cpu = collect_cpu()
    mem = collect_memory()
    board = collect_motherboard()
    gpu = collect_graphics()
    sensors = collect_sensors()
    overview = collect_overview(cpu, mem, board, gpu)
    return {
        "Overview": overview,
        "CPU": cpu,
        "Memory": mem,
        "Motherboard": board,
        "Graphics": gpu,
        "Sensors": sensors,
    }


def report_text(data, bench_result=""):
    lines = [f"{APP_NAME} {VERSION}", f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", ""]
    for section, payload in data.items():
        lines.append(f"[{section}]")
        for group, rows in payload.items():
            if group == "raw":
                continue
            if group != "main":
                lines.append(f"  {group.title()}")
            for k, v in rows:
                lines.append(f"  {k}: {v}")
        lines.append("")
    if bench_result:
        lines.append("[Bench]")
        lines.append(bench_result)
    return "\n".join(lines)


class InfoTable(Gtk.TreeView):
    def __init__(self):
        self.store = Gtk.ListStore(str, str)
        super().__init__(model=self.store)
        self.set_headers_visible(True)
        self.set_rules_hint(True)
        self.renderer1 = Gtk.CellRendererText()
        self.renderer1.set_property("weight", Pango.Weight.BOLD)
        col1 = Gtk.TreeViewColumn("Item", self.renderer1, text=0)
        col1.set_resizable(True)
        col1.set_min_width(180)
        self.append_column(col1)
        self.renderer2 = Gtk.CellRendererText()
        self.renderer2.set_property("wrap-mode", Pango.WrapMode.WORD_CHAR)
        self.renderer2.set_property("wrap-width", 520)
        col2 = Gtk.TreeViewColumn("Value", self.renderer2, text=1)
        col2.set_resizable(True)
        col2.set_expand(True)
        self.append_column(col2)

    def set_colors(self, foreground, background):
        for renderer in (self.renderer1, self.renderer2):
            renderer.set_property("foreground", foreground)
            renderer.set_property("foreground-set", True)
            renderer.set_property("background", background)
            renderer.set_property("background-set", True)

    def set_rows(self, rows):
        self.store.clear()
        for k, v in rows:
            self.store.append([str(k), str(v)])


SKINS = {
    "White CPU-Z": {
        "cell_fg": "#111111",
        "cell_bg": "#ffffff",
        "css": b"""
        * { color: #111111; }
        window, notebook, notebook > stack, scrolledwindow, viewport, box, .content { background: #ffffff; background-color: #ffffff; color: #111111; }
        headerbar, headerbar.titlebar, .app-titlebar, .app-titlebar.titlebar { background: #ffffff; background-color: #ffffff; color: #000000; border-bottom: 1px solid #d6dde8; box-shadow: none; }
        headerbar label, headerbar.titlebar label, .app-titlebar label { color: #12356f; font-weight: 700; }
        headerbar button, headerbar button.titlebutton { background: #ffffff; background-color: #ffffff; color: #000000; border: 1px solid #cbd5e1; }
        headerbar button label, headerbar button image { color: #000000; }
        .topbar { background: #ffffff; background-color: #ffffff; border-bottom: 1px solid #d6dde8; padding: 6px; }
        .title { color: #12356f; font-weight: 700; font-size: 18px; }
        .subtitle { color: #263241; font-size: 10px; }
        label, .content label { color: #111111; }
        frame { margin: 8px; border: 0; border-radius: 0; background: #ffffff; background-color: #ffffff; color: #111111; }
        frame > border { border: 0; border-radius: 0; }
        frame > label { color: #12356f; font-weight: 700; }
        treeview, treeview.view { background: #ffffff; background-color: #ffffff; color: #111111; }
        treeview.view:selected, treeview.view:selected:focus { background: #dbeafe; background-color: #dbeafe; color: #000000; }
        treeview header button, treeview.view header button { background: #f8fafc; background-color: #f8fafc; color: #000000; border: 1px solid #cbd5e1; }
        treeview header button label, treeview.view header button label { color: #000000; }
        button { padding: 5px 10px; background: #ffffff; background-color: #ffffff; color: #000000; border: 1px solid #cbd5e1; border-radius: 3px; }
        button label { color: #000000; }
        combobox, combobox *, combobox cellview, combobox cellview text { background: #ffffff; background-color: #ffffff; color: #000000; }
        combobox button { background: #ffffff; background-color: #ffffff; color: #000000; border: 1px solid #cbd5e1; }
        combobox button label, combobox button cellview, combobox button arrow { color: #000000; background: #ffffff; background-color: #ffffff; }
        menu, menuitem, combobox menu, combobox menuitem, popover, popover contents, window.popup { background: #ffffff; background-color: #ffffff; color: #000000; border: 1px solid #cbd5e1; }
        menuitem label, menu label, popover label, combobox menuitem label { color: #000000; background: transparent; }
        menuitem:hover, menuitem:selected, combobox menuitem:hover, combobox menuitem:selected { background: #dbeafe; background-color: #dbeafe; color: #000000; }
        menuitem:hover label, menuitem:selected label, combobox menuitem:hover label, combobox menuitem:selected label { color: #000000; }
        notebook > header { background: #edf2f8; background-color: #edf2f8; border-bottom: 1px solid #cbd5e1; }
        notebook tab { padding: 6px 12px; background: #f8fafc; background-color: #f8fafc; border: 1px solid #cbd5e1; }
        notebook tab label { color: #000000; }
        notebook tab:checked { background: #ffffff; background-color: #ffffff; }
        notebook tab:checked label { color: #000000; font-weight: 700; }
        .status { color: #111111; background: #eef3fa; background-color: #eef3fa; padding: 5px; border-top: 1px solid #cbd5e1; }
        """,
    },
    "Govibe Dark": {
        "cell_fg": "#e5edf7",
        "cell_bg": "#111827",
        "css": b"""
        * { color: #e5edf7; }
        window { background: #0b1020; color: #e5edf7; }
        headerbar, headerbar.titlebar, .app-titlebar, .app-titlebar.titlebar { background: #0b1020; background-color: #0b1020; color: #e5edf7; border-bottom: 1px solid #38bdf8; }
        headerbar label, headerbar.titlebar label, .app-titlebar label { color: #7dd3fc; font-weight: 700; }
        headerbar button, headerbar button.titlebutton { background: #1e293b; background-color: #1e293b; color: #e5edf7; border: 1px solid #38bdf8; }
        .topbar { background: #111827; border-bottom: 1px solid #38bdf8; padding: 6px; }
        .title { color: #7dd3fc; font-weight: 700; font-size: 18px; }
        .subtitle { color: #cbd5e1; font-size: 10px; }
        label { color: #e5edf7; }
        frame { margin: 8px; border: 1px solid #38bdf8; border-radius: 4px; background: #111827; color: #e5edf7; }
        frame > border { border: 1px solid #38bdf8; border-radius: 4px; }
        frame > label { color: #7dd3fc; font-weight: 700; }
        treeview, treeview.view { background: #111827; color: #e5edf7; }
        treeview.view:selected, treeview.view:selected:focus { background: #0ea5e9; color: #ffffff; }
        treeview header button, treeview.view header button { background: #1e293b; color: #e5edf7; border: 1px solid #38bdf8; }
        treeview header button label, treeview.view header button label { color: #e5edf7; }
        button { padding: 5px 10px; background: #1e293b; color: #e5edf7; border: 1px solid #38bdf8; border-radius: 3px; }
        button label { color: #e5edf7; }
        combobox, combobox *, combobox cellview { background: #1e293b; background-color: #1e293b; color: #e5edf7; }
        combobox button { background: #1e293b; color: #e5edf7; border: 1px solid #38bdf8; }
        combobox button label, combobox button cellview, combobox button arrow { color: #e5edf7; }
        menu, menuitem, combobox menu, combobox menuitem, popover, popover contents, window.popup { background: #0f172a; background-color: #0f172a; color: #e5edf7; border: 1px solid #38bdf8; }
        menuitem label, menu label, popover label, combobox menuitem label { color: #e5edf7; background: transparent; }
        menuitem:hover, menuitem:selected, combobox menuitem:hover, combobox menuitem:selected { background: #1e40af; background-color: #1e40af; color: #ffffff; }
        menuitem:hover label, menuitem:selected label, combobox menuitem:hover label, combobox menuitem:selected label { color: #ffffff; }
        notebook > header { background: #0f172a; border-bottom: 1px solid #38bdf8; }
        notebook tab { padding: 6px 12px; background: #111827; border: 1px solid #334155; }
        notebook tab label { color: #e5edf7; }
        notebook tab:checked { background: #1e293b; border-color: #38bdf8; }
        notebook tab:checked label { color: #7dd3fc; font-weight: 700; }
        .status { color: #e5edf7; background: #111827; padding: 5px; border-top: 1px solid #38bdf8; }
        """,
    },
    "Terminal Green": {
        "cell_fg": "#00ff41",
        "cell_bg": "#000000",
        "css": b"""
        * { color: #00ff41; }
        window { background: #000000; color: #00ff41; }
        headerbar, headerbar.titlebar, .app-titlebar, .app-titlebar.titlebar { background: #000000; background-color: #000000; color: #00ff41; border-bottom: 1px solid #00ff41; }
        headerbar label, headerbar.titlebar label, .app-titlebar label { color: #00ff41; font-weight: 700; }
        headerbar button, headerbar button.titlebutton { background: #001a05; background-color: #001a05; color: #00ff41; border: 1px solid #00ff41; }
        .topbar { background: #000000; background-color: #000000; border-bottom: 1px solid #00ff41; padding: 6px; }
        .title { color: #00ff41; font-weight: 700; font-size: 18px; }
        .subtitle { color: #00cc33; font-size: 10px; }
        label { color: #00ff41; }
        frame { margin: 8px; border: 1px solid #00ff41; border-radius: 3px; background: #000000; color: #00ff41; }
        frame > border { border: 1px solid #00ff41; border-radius: 3px; }
        frame > label { color: #00ff41; font-weight: 700; }
        treeview, treeview.view { background: #000000; color: #00ff41; }
        treeview.view:selected, treeview.view:selected:focus { background: #003b11; color: #ccffd8; }
        treeview header button, treeview.view header button { background: #001a05; color: #00ff41; border: 1px solid #00ff41; }
        treeview header button label, treeview.view header button label { color: #00ff41; }
        button { padding: 5px 10px; background: #001a05; color: #00ff41; border: 1px solid #00ff41; border-radius: 3px; }
        button label { color: #00ff41; }
        combobox, combobox *, combobox cellview { background: #001a05; background-color: #001a05; color: #00ff41; }
        combobox button { background: #001a05; color: #00ff41; border: 1px solid #00ff41; }
        combobox button label, combobox button cellview, combobox button arrow { color: #00ff41; }
        menu, menuitem, combobox menu, combobox menuitem, popover, popover contents, window.popup { background: #000000; background-color: #000000; color: #00ff41; border: 1px solid #00ff41; }
        menuitem label, menu label, popover label, combobox menuitem label { color: #00ff41; background: transparent; }
        menuitem:hover, menuitem:selected, combobox menuitem:hover, combobox menuitem:selected { background: #003b11; background-color: #003b11; color: #ccffd8; }
        menuitem:hover label, menuitem:selected label, combobox menuitem:hover label, combobox menuitem:selected label { color: #ccffd8; }
        notebook > header { background: #000000; border-bottom: 1px solid #00ff41; }
        notebook tab { padding: 6px 12px; background: #001a05; border: 1px solid #008f11; }
        notebook tab label { color: #00ff41; }
        notebook tab:checked { background: #003b11; border-color: #00ff41; }
        notebook tab:checked label { color: #ccffd8; font-weight: 700; }
        .status { color: #00ff41; background: #001a05; padding: 5px; border-top: 1px solid #00ff41; }
        """,
    },
}

class GovibePCSpecs(Gtk.Application):
    def __init__(self):
        super().__init__(application_id=APP_ID)
        self.window = None
        self.data = {}
        self.tables = {}
        self.status = None
        self.cpu_bench_label = None
        self.cpu_bench_button = None
        self.cpu_bench_progress = None
        self.cpu_bench_result = ""
        self.gpu_bench_label = None
        self.gpu_bench_button = None
        self.gpu_bench_progress = None
        self.gpu_bench_result = ""
        self.bench_result = ""
        self.css_provider = None
        self.skin_combo = None
        self.current_skin = "White CPU-Z"

    def do_activate(self):
        if self.window:
            self.window.present()
            return
        self.build_ui()
        self.refresh()
        self.window.show_all()
        self.window.present()

    def build_ui(self):
        self.window = Gtk.ApplicationWindow(application=self)
        self.window.set_title(APP_NAME)
        self.window.set_default_size(920, 650)
        self.window.set_position(Gtk.WindowPosition.CENTER)
        self.headerbar = Gtk.HeaderBar()
        self.headerbar.set_title(APP_NAME)
        self.headerbar.set_show_close_button(True)
        self.headerbar.get_style_context().add_class("app-titlebar")
        self.window.set_titlebar(self.headerbar)

        self.css_provider = Gtk.CssProvider()
        Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(), self.css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
        self.apply_skin(self.current_skin)

        root = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
        self.window.add(root)

        top = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
        top.get_style_context().add_class("topbar")
        root.pack_start(top, False, False, 0)

        logo = Gtk.Image.new_from_icon_name("computer-symbolic", Gtk.IconSize.DIALOG)
        top.pack_start(logo, False, False, 4)

        titlebox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
        title = Gtk.Label(label=APP_NAME)
        title.set_xalign(0)
        title.get_style_context().add_class("title")
        sub = Gtk.Label(label="All in one Linux hardware viewer")
        sub.set_xalign(0)
        sub.get_style_context().add_class("subtitle")
        titlebox.pack_start(title, False, False, 0)
        titlebox.pack_start(sub, False, False, 0)
        top.pack_start(titlebox, True, True, 0)

        skin_label = Gtk.Label(label="Skin")
        skin_label.set_xalign(0)
        top.pack_start(skin_label, False, False, 0)
        self.skin_combo = Gtk.ComboBoxText()
        for skin_name in SKINS.keys():
            self.skin_combo.append_text(skin_name)
        self.skin_combo.set_active(0)
        self.skin_combo.connect("changed", self.skin_changed)
        top.pack_start(self.skin_combo, False, False, 0)

        refresh_btn = Gtk.Button(label="Refresh")
        refresh_btn.connect("clicked", lambda b: self.refresh())
        copy_btn = Gtk.Button(label="Copy Report")
        copy_btn.connect("clicked", lambda b: self.copy_report())
        export_btn = Gtk.Button(label="Export Report")
        export_btn.connect("clicked", lambda b: self.export_report())
        top.pack_start(refresh_btn, False, False, 0)
        top.pack_start(copy_btn, False, False, 0)
        top.pack_start(export_btn, False, False, 0)

        self.notebook = Gtk.Notebook()
        root.pack_start(self.notebook, True, True, 0)

        for name in ["Overview", "CPU", "Memory", "Motherboard", "Graphics", "Sensors"]:
            self.add_info_tab(name)
        self.add_bench_tab()
        self.add_about_tab()

        self.status = Gtk.Label(label="Ready")
        self.status.set_xalign(0)
        self.status.get_style_context().add_class("status")
        root.pack_start(self.status, False, False, 0)

    def skin_changed(self, combo):
        text = combo.get_active_text()
        if text:
            self.apply_skin(text)

    def apply_skin(self, skin_name):
        skin = SKINS.get(skin_name) or SKINS["White CPU-Z"]
        self.current_skin = skin_name if skin_name in SKINS else "White CPU-Z"
        if self.css_provider:
            self.css_provider.load_from_data(skin["css"])
        for table in self.tables.values():
            table.set_colors(skin["cell_fg"], skin["cell_bg"])

    def add_info_tab(self, name):
        scroll = Gtk.ScrolledWindow()
        scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
        box.get_style_context().add_class("content")
        scroll.add(box)
        main_frame = Gtk.Frame(label=name)
        table = InfoTable()
        skin = SKINS.get(self.current_skin, SKINS["White CPU-Z"])
        table.set_colors(skin["cell_fg"], skin["cell_bg"])
        main_frame.add(table)
        box.pack_start(main_frame, True, True, 0)
        self.tables[(name, "main")] = table
        if name == "CPU":
            cache_frame = Gtk.Frame(label="Caches")
            cache_table = InfoTable()
            cache_table.set_colors(skin["cell_fg"], skin["cell_bg"])
            cache_frame.add(cache_table)
            box.pack_start(cache_frame, False, True, 0)
            self.tables[(name, "caches")] = cache_table
            feature_frame = Gtk.Frame(label="Features")
            feature_table = InfoTable()
            feature_table.set_colors(skin["cell_fg"], skin["cell_bg"])
            feature_frame.add(feature_table)
            box.pack_start(feature_frame, False, True, 0)
            self.tables[(name, "features")] = feature_table
        if name == "Memory":
            mod_frame = Gtk.Frame(label="Modules")
            mod_table = InfoTable()
            mod_table.set_colors(skin["cell_fg"], skin["cell_bg"])
            mod_frame.add(mod_table)
            box.pack_start(mod_frame, False, True, 0)
            self.tables[(name, "modules")] = mod_table
        self.notebook.append_page(scroll, Gtk.Label(label=name))

    def add_bench_tab(self):
        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=14)
        box.get_style_context().add_class("content")
        box.set_border_width(18)

        title = Gtk.Label(label="CPU and GPU benchmarks")
        title.set_xalign(0)
        title.get_style_context().add_class("title")
        desc = Gtk.Label(label="Each benchmark runs for 15 seconds. The GPU test uses an OpenGL workload when glxgears is available.")
        desc.set_xalign(0)
        desc.set_line_wrap(True)
        box.pack_start(title, False, False, 0)
        box.pack_start(desc, False, False, 0)

        cpu_frame = Gtk.Frame(label="CPU Benchmark")
        cpu_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
        cpu_box.set_border_width(10)
        self.cpu_bench_button = Gtk.Button(label="Run CPU Benchmark 15 sec")
        self.cpu_bench_button.connect("clicked", self.run_cpu_benchmark)
        self.cpu_bench_progress = Gtk.ProgressBar()
        self.cpu_bench_label = Gtk.Label(label="No CPU benchmark result yet")
        self.cpu_bench_label.set_xalign(0)
        self.cpu_bench_label.set_line_wrap(True)
        cpu_box.pack_start(self.cpu_bench_button, False, False, 0)
        cpu_box.pack_start(self.cpu_bench_progress, False, False, 0)
        cpu_box.pack_start(self.cpu_bench_label, False, False, 0)
        cpu_frame.add(cpu_box)
        box.pack_start(cpu_frame, False, True, 0)

        gpu_frame = Gtk.Frame(label="GPU Benchmark")
        gpu_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
        gpu_box.set_border_width(10)
        self.gpu_bench_button = Gtk.Button(label="Run GPU Benchmark 15 sec")
        self.gpu_bench_button.connect("clicked", self.run_gpu_benchmark)
        self.gpu_bench_progress = Gtk.ProgressBar()
        self.gpu_bench_label = Gtk.Label(label="No GPU benchmark result yet")
        self.gpu_bench_label.set_xalign(0)
        self.gpu_bench_label.set_line_wrap(True)
        gpu_box.pack_start(self.gpu_bench_button, False, False, 0)
        gpu_box.pack_start(self.gpu_bench_progress, False, False, 0)
        gpu_box.pack_start(self.gpu_bench_label, False, False, 0)
        gpu_frame.add(gpu_box)
        box.pack_start(gpu_frame, False, True, 0)

        self.notebook.append_page(box, Gtk.Label(label="Bench"))

    def add_about_tab(self):
        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
        box.get_style_context().add_class("content")
        box.set_border_width(18)
        title = Gtk.Label(label=f"{APP_NAME} {VERSION}")
        title.set_xalign(0)
        title.get_style_context().add_class("title")
        text = Gtk.Label(label=(
            "Govibe PC Specs shows the most important Linux hardware information in one place.\n\n"
            "Tabs: Overview, CPU, Memory, Motherboard, Graphics, Sensors, and Bench.\n\n"
            "Bundled dependencies install lm-sensors, mesa-utils, vulkan-tools, pciutils and X display tools for deeper detection.\n"
            "RAM slot, frequency, voltage, manufacturer and part number are read by the bundled Govibe C SMBIOS reader.\n"
            "GPU memory chip brand probing is handled by the bundled Govibe C GPU hardware detector.\n"
            "CPU and GPU benchmarks run for 15 seconds each.\n\n"
            "govibe.org"
        ))
        text.set_xalign(0)
        text.set_yalign(0)
        text.set_line_wrap(True)
        box.pack_start(title, False, False, 0)
        box.pack_start(text, False, False, 0)
        self.notebook.append_page(box, Gtk.Label(label="About"))

    def set_status(self, text):
        if self.status:
            self.status.set_text(text)

    def refresh(self):
        self.set_status("Scanning hardware...")
        while Gtk.events_pending():
            Gtk.main_iteration_do(False)
        try:
            self.data = collect_all()
            for section, payload in self.data.items():
                for group, rows in payload.items():
                    table = self.tables.get((section, group))
                    if table:
                        table.set_rows(rows)
            self.set_status("Hardware scan complete")
        except Exception as exc:
            self.set_status(f"Scan error: {exc}")

    def copy_report(self):
        text = report_text(self.data, self.bench_result)
        Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD).set_text(text, -1)
        self.set_status("Report copied to clipboard")

    def export_report(self):
        dialog = Gtk.FileChooserDialog(
            title="Export Report",
            parent=self.window,
            action=Gtk.FileChooserAction.SAVE,
            buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE, Gtk.ResponseType.OK),
        )
        dialog.set_current_name("GovibePCSpecs-report.txt")
        response = dialog.run()
        if response == Gtk.ResponseType.OK:
            path = dialog.get_filename()
            try:
                Path(path).write_text(report_text(self.data, self.bench_result), encoding="utf-8")
                self.set_status(f"Report saved: {path}")
            except Exception as exc:
                self.set_status(f"Export failed: {exc}")
        dialog.destroy()

    def update_bench_report(self):
        parts = []
        if self.cpu_bench_result:
            parts.append("CPU: " + self.cpu_bench_result)
        if self.gpu_bench_result:
            parts.append("GPU: " + self.gpu_bench_result)
        self.bench_result = "\n".join(parts)

    def run_cpu_benchmark(self, button):
        self.cpu_bench_button.set_sensitive(False)
        self.cpu_bench_progress.set_fraction(0)
        self.cpu_bench_label.set_text("CPU benchmark running for 15 seconds...")
        self.set_status("CPU benchmark running")

        def worker():
            duration = 15.0
            start = time.perf_counter()
            deadline = start + duration
            ops = 0
            checksum = 0.0
            chunk = 18000
            while True:
                now = time.perf_counter()
                if now >= deadline:
                    break
                local = 0.0
                for n in range(1, chunk):
                    local += math.sqrt(n) * math.sin((n + ops) % 360) * math.cos(n % 91)
                checksum += local
                ops += chunk - 1
                elapsed = time.perf_counter() - start
                GLib.idle_add(self.cpu_bench_progress.set_fraction, min(elapsed / duration, 1.0))
            elapsed = time.perf_counter() - start
            score = int(ops / max(elapsed, 0.001))
            result = f"Elapsed: {elapsed:.2f} seconds | Score: {score:,} math ops/sec | Checksum: {checksum:.2f}"
            self.cpu_bench_result = result
            self.update_bench_report()
            GLib.idle_add(self.cpu_bench_done, result)

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

    def cpu_bench_done(self, result):
        self.cpu_bench_progress.set_fraction(1.0)
        self.cpu_bench_label.set_text(result)
        self.cpu_bench_button.set_sensitive(True)
        self.set_status("CPU benchmark complete")
        return False

    def run_gpu_benchmark(self, button):
        self.gpu_bench_button.set_sensitive(False)
        self.gpu_bench_progress.set_fraction(0)
        self.gpu_bench_label.set_text("GPU benchmark running for 15 seconds...")
        self.set_status("GPU benchmark running")

        def worker():
            duration = 15.0
            if not shutil.which("glxgears"):
                result = "GPU benchmark unavailable. mesa-utils/glxgears is not installed."
                self.gpu_bench_result = result
                self.update_bench_report()
                GLib.idle_add(self.gpu_bench_done, result)
                return
            env = os.environ.copy()
            env["vblank_mode"] = "0"
            env["__GL_SYNC_TO_VBLANK"] = "0"
            lines = []
            start = time.perf_counter()
            proc = None
            try:
                proc = subprocess.Popen(
                    ["glxgears", "-info"],
                    stdout=subprocess.PIPE,
                    stderr=subprocess.STDOUT,
                    text=True,
                    env=env,
                    bufsize=1,
                )
                while time.perf_counter() - start < duration:
                    elapsed = time.perf_counter() - start
                    GLib.idle_add(self.gpu_bench_progress.set_fraction, min(elapsed / duration, 1.0))
                    if proc.poll() is not None:
                        break
                    if proc.stdout:
                        readable, _, _ = select.select([proc.stdout], [], [], 0.2)
                        if readable:
                            line = proc.stdout.readline()
                            if line:
                                lines.append(line.strip())
                    else:
                        time.sleep(0.2)
                if proc and proc.poll() is None:
                    proc.terminate()
                    try:
                        proc.wait(timeout=2)
                    except Exception:
                        proc.kill()
                if proc and proc.stdout:
                    try:
                        for line in proc.stdout.readlines():
                            if line.strip():
                                lines.append(line.strip())
                    except Exception:
                        pass
            except Exception as exc:
                result = f"GPU benchmark failed: {exc}"
                self.gpu_bench_result = result
                self.update_bench_report()
                GLib.idle_add(self.gpu_bench_done, result)
                return

            text = "\n".join(lines)
            frame_total = 0
            second_total = 0.0
            fps_values = []
            for m in re.finditer(r"(\d+)\s+frames\s+in\s+([0-9.]+)\s+seconds\s*=\s*([0-9.]+)\s+FPS", text, re.I):
                frame_total += int(m.group(1))
                second_total += float(m.group(2))
                fps_values.append(float(m.group(3)))
            renderer = ""
            for line in lines:
                if "GL_RENDERER" in line or "OpenGL renderer" in line:
                    renderer = line.split("=", 1)[-1].strip()
                    break
            if not renderer:
                glx = parse_key_value(run_cmd(["glxinfo", "-B"], timeout=4)) if shutil.which("glxinfo") else {}
                renderer = glx.get("OpenGL renderer string", "")
            elapsed = max(time.perf_counter() - start, 0.001)
            if fps_values:
                avg_fps = frame_total / second_total if second_total else sum(fps_values) / len(fps_values)
                result = f"Elapsed: {elapsed:.2f} seconds | Average: {avg_fps:.1f} FPS | Frames: {frame_total:,}"
                if renderer:
                    result += f" | Renderer: {renderer}"
            else:
                result = "OpenGL benchmark completed but no FPS output was captured."
                if renderer:
                    result += f" Renderer: {renderer}"
            self.gpu_bench_result = result
            self.update_bench_report()
            GLib.idle_add(self.gpu_bench_done, result)

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

    def gpu_bench_done(self, result):
        self.gpu_bench_progress.set_fraction(1.0)
        self.gpu_bench_label.set_text(result)
        self.gpu_bench_button.set_sensitive(True)
        self.set_status("GPU benchmark complete")
        return False


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


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