#!/usr/bin/env python3
"""PC Health Monitor v2.0 — Windows 11"""

import sys, os, subprocess, threading, time, platform, webbrowser, tempfile, json, socket
from datetime import datetime, date
from collections import deque

# ═══════════════════════════════════════════════════════
#  Dependency bootstrap
# ═══════════════════════════════════════════════════════
REQUIRED = ['customtkinter', 'psutil', 'wmi', 'fpdf2']

# pip name → import name (when they differ)
_PKG_IMPORT = {'fpdf2': 'fpdf'}

def _ok(pkg):
    try: __import__(_PKG_IMPORT.get(pkg, pkg)); return True
    except ImportError: return False

def _bootstrap():
    missing = [p for p in REQUIRED if not _ok(p)]
    if not missing: return True
    import tkinter as tk
    from tkinter import messagebox
    r = tk.Tk(); r.withdraw(); r.attributes('-topmost', True)
    if not messagebox.askyesno("PC Health Monitor",
        "Pachete necesare:\n" + "\n".join(f"  • {p}" for p in missing) +
        "\n\nInstalati acum?", parent=r):
        r.destroy(); return False
    r.destroy()
    r2 = tk.Tk(); r2.title("Se instaleaza..."); r2.geometry("360x100")
    r2.resizable(False, False); r2.attributes('-topmost', True)
    tk.Label(r2, text="Se instaleaza pachetele...", pady=12, font=('Arial', 10)).pack()
    lbl = tk.Label(r2, text="", fg='blue'); lbl.pack(); r2.update()
    flags = getattr(subprocess, 'CREATE_NO_WINDOW', 0)
    for p in missing:
        lbl.config(text=f"pip install {p}"); r2.update()
        try:
            subprocess.check_call([sys.executable, '-m', 'pip', 'install', p],
                                  stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
                                  creationflags=flags)
        except Exception as e:
            r2.destroy()
            rr = tk.Tk(); rr.withdraw()
            messagebox.showerror("Eroare", f"Nu s-a putut instala {p}:\n{e}")
            rr.destroy(); return False
    r2.destroy(); return True

if not _bootstrap(): sys.exit(0)

import customtkinter as ctk
import tkinter as tk
from tkinter import ttk, filedialog
import psutil

try:
    import wmi as _wmi_lib; WMI_OK = True
except ImportError:
    _wmi_lib = None; WMI_OK = False

try:
    import pystray
    from PIL import Image, ImageDraw
    TRAY_OK = True
except ImportError:
    TRAY_OK = False

# ═══════════════════════════════════════════════════════
#  Constants
# ═══════════════════════════════════════════════════════
VERSION = "3.0"
TITLE   = "PC Health Monitor"
HIST    = 90  # seconds of history

HC = {'good': '#00C853', 'warning': '#FFB300', 'bad': '#F44336', 'unknown': '#757575'}
GRAPH_BG   = '#0a0e1a'
GRAPH_GRID = '#1a1f30'

THR = {
    'cpu':      {'warning': 75, 'bad': 92},
    'ram':      {'warning': 80, 'bad': 93},
    'disk_pct': {'warning': 85, 'bad': 95},
    'cpu_temp': {'warning': 75, 'bad': 88},
    'gpu_temp': {'warning': 75, 'bad': 88},
    'hdd_temp': {'warning': 45, 'bad': 55},
    'ssd_temp': {'warning': 55, 'bad': 65},
}

MEM_TYPE = {
    '0': 'Unknown', '2': 'DRAM', '20': 'DDR', '21': 'DDR2',
    '24': 'DDR3', '26': 'DDR4', '34': 'DDR5',
}

# ═══════════════════════════════════════════════════════
#  Helpers
# ═══════════════════════════════════════════════════════
def fmt_bytes(b):
    if not b: return '0 B'
    for u in ['B', 'KB', 'MB', 'GB', 'TB']:
        if b < 1024: return f"{b:.1f} {u}"
        b /= 1024
    return f"{b:.1f} PB"

def fmt_speed(kb):
    if kb >= 1024 * 1024: return f"{kb/1024/1024:.2f} GB/s"
    if kb >= 1024:        return f"{kb/1024:.1f} MB/s"
    return f"{kb:.0f} KB/s"

def health_of(val, key):
    t = THR.get(key, {})
    if val >= t.get('bad', 9999):    return 'bad'
    if val >= t.get('warning', 9999): return 'warning'
    return 'good'

_PS_MAX_OUT = 524_288  # 512 KB — previne output masiv din PS rogue

def _run_ps(cmd, timeout=8):
    try:
        flags = getattr(subprocess, 'CREATE_NO_WINDOW', 0)
        r = subprocess.run(
            ['powershell', '-NonInteractive', '-NoProfile', '-Command', cmd],
            capture_output=True, text=True, timeout=timeout, creationflags=flags)
        out = r.stdout.strip()
        return out[:_PS_MAX_OUT] if len(out) > _PS_MAX_OUT else out
    except Exception:
        return ''

# History
_HIST_DIR = os.path.join(os.environ.get('LOCALAPPDATA', os.path.expanduser('~')),
                          'PCHealthMonitor', 'history')

def _save_history(data):
    try:
        os.makedirs(_HIST_DIR, exist_ok=True)
        ts   = datetime.now().strftime('%Y%m%d_%H%M%S')
        path = os.path.join(_HIST_DIR, f'check_{ts}.json')
        # Scrie atomic: temp file → rename
        tmp = path + '.tmp'
        with open(tmp, 'w', encoding='utf-8') as f:
            json.dump(data, f, ensure_ascii=False, default=str)
        os.replace(tmp, path)
        # Pastreaza ultimele 30
        files = sorted(x for x in os.listdir(_HIST_DIR)
                       if x.startswith('check_') and x.endswith('.json'))
        for old in files[:-30]:
            try: os.remove(os.path.join(_HIST_DIR, old))
            except Exception: pass
    except Exception: pass

def _load_last_history():
    try:
        files = sorted(x for x in os.listdir(_HIST_DIR)
                       if x.startswith('check_') and x.endswith('.json'))
        if len(files) < 2: return None
        path = os.path.join(_HIST_DIR, files[-2])
        if os.path.getsize(path) > 10_485_760:   # ignora fisiere > 10 MB
            return None
        with open(path, 'r', encoding='utf-8') as f:
            result = json.load(f)
        if not isinstance(result, dict):
            return None
        return result
    except Exception: return None

def _calc_health_score(d):
    cats, score = [], 0

    def cat(name, got, mx):
        nonlocal score
        score += got
        cats.append((name, got, mx))

    # Storage (20)
    disks = d.get('storage', {}).get('disks', [])
    parts = d.get('storage', {}).get('parts', [])
    if disks:
        bad  = any(x['health'] == 'bad'     for x in disks)
        warn = any(x['health'] == 'warning'  for x in disks)
        full = any(p['pct'] > 90             for p in parts)
        cat('Stocare', 0 if bad else 10 if (warn or full) else 20, 20)

    # RAM (10)
    ram = d.get('memory', {}).get('pct', 0)
    cat('RAM', max(0, 10 - max(0, int((ram - 70) / 3))), 10)

    # CPU temp (10)
    temps = d.get('system', {}).get('temps', [])
    avg_t = sum(temps) / len(temps) if temps else 0
    cat('Temp CPU', 10 if not temps else (10 if avg_t < 70 else 6 if avg_t < 80 else 2), 10)

    # Defender (15)
    df = d.get('security', {}).get('defender', {})
    if df.get('available'):
        s = 0
        if df.get('enabled') and df.get('rt_protection'): s += 9
        elif df.get('enabled'): s += 4
        s += (6 if df.get('sig_age_days', 99) <= 7 else
              3 if df.get('sig_age_days', 99) <= 30 else 0)
        cat('Defender', s, 15)

    # Firewall (10)
    fw = d.get('security', {}).get('firewall', [])
    if fw:
        n_on = sum(1 for x in fw if x.get('enabled'))
        cat('Firewall', round(10 * n_on / len(fw)), 10)

    # Activation (5)
    act = d.get('security', {}).get('activation', '')
    if act:
        cat('Activare', 5 if act == 'Activat' else 0, 5)

    # Updates (10)
    upd = d.get('security', {}).get('updates', [])
    if upd:
        try:
            newest = max(date.fromisoformat(u['date'][:10])
                         for u in upd if len(u.get('date','')) >= 10)
            age = (date.today() - newest).days
            s = 10 if age <= 30 else 7 if age <= 90 else 3 if age <= 180 else 0
        except: s = 5
        cat('Actualizari', s, 10)

    # Internet (5)
    if d.get('network', {}).get('internet') is not None:
        cat('Internet', 5 if d['network']['internet'] else 0, 5)

    # Critical events (10)
    ev = d.get('perf_extras', {}).get('events', [])
    cat('Evenimente', 10 if not ev else 6 if len(ev) <= 2 else 2, 10)

    max_total = sum(c[2] for c in cats)
    final = round(score / max_total * 100) if max_total else 0
    grade = 'good' if final >= 75 else ('warning' if final >= 50 else 'bad')
    return final, grade, cats

# ═══════════════════════════════════════════════════════
#  MiniGraph — Canvas live chart
# ═══════════════════════════════════════════════════════
class MiniGraph(tk.Canvas):
    def __init__(self, parent, data: deque, color='#4f8ef7',
                 width=300, height=80, unit='%', scale=100.0, **kw):
        super().__init__(parent, width=width, height=height,
                         bg=GRAPH_BG, highlightthickness=0, **kw)
        self.data  = data
        self.color = color
        self.unit  = unit
        self.scale = scale
        self.bind('<Configure>', lambda _e: self._draw())

    def refresh(self): self._draw()

    def _draw(self):
        self.delete('all')
        pts = list(self.data)
        w = self.winfo_width() or 300
        h = self.winfo_height() or 80
        if w < 4 or h < 4 or len(pts) < 2: return

        cap = self.data.maxlen or len(pts)
        n   = len(pts)
        mv  = self.scale if self.unit == '%' else max(self.scale, max(pts) * 1.15) if pts else self.scale

        for p in (25, 50, 75):
            y = int(h - (p / 100 * (h - 4)) - 2)
            self.create_line(0, y, w, y, fill=GRAPH_GRID, dash=(2, 5))

        xs = [int(i * w / (cap - 1)) for i in range(cap - n, cap)]
        ys = [max(2, min(h - 2, int(h - 2 - (p / mv * (h - 6))))) for p in pts]

        poly = [0, h] + [c for xy in zip(xs, ys) for c in xy] + [xs[-1], h]
        self.create_polygon(poly, fill=self.color + '28', outline='')
        self.create_line([c for xy in zip(xs, ys) for c in xy],
                         fill=self.color, width=2, smooth=True)

        cur = pts[-1]
        if self.unit == '%':
            lbl = f'{cur:.0f}%'
        elif self.unit == 'KB/s':
            lbl = fmt_speed(cur)
        elif self.unit == 'MB/s':
            lbl = (f'{cur:.1f} MB/s' if cur >= 0.1 else f'{cur*1024:.0f} KB/s')
        elif self.unit == '°C':
            lbl = f'{cur:.0f}°C'
        else:
            lbl = f'{cur:.1f}{self.unit}'

        self.create_text(w - 5, 5, text=lbl, fill=self.color,
                         anchor='ne', font=('Consolas', 9, 'bold'))

# ═══════════════════════════════════════════════════════
#  DataStore — thread-safe ring buffers
# ═══════════════════════════════════════════════════════
class DataStore:
    def __init__(self):
        self.cpu    = deque([0.0] * HIST, maxlen=HIST)
        self.ram    = deque([0.0] * HIST, maxlen=HIST)
        self.gpu_t  = deque([0.0] * HIST, maxlen=HIST)
        self.gpu_u  = deque([0.0] * HIST, maxlen=HIST)
        self.net_rx = deque([0.0] * HIST, maxlen=HIST)
        self.net_tx = deque([0.0] * HIST, maxlen=HIST)
        self.disk_r = deque([0.0] * HIST, maxlen=HIST)
        self.disk_w = deque([0.0] * HIST, maxlen=HIST)
        self.cores: list[deque] = []
        self._lock  = threading.Lock()

    def push_cpu(self, total, per_core):
        with self._lock:
            self.cpu.append(total)
            while len(self.cores) < len(per_core):
                self.cores.append(deque([0.0] * HIST, maxlen=HIST))
            for i, v in enumerate(per_core):
                self.cores[i].append(v)

    def push(self, attr, val):
        with self._lock:
            getattr(self, attr).append(val)

# ═══════════════════════════════════════════════════════
#  Sampler — background 1 Hz sampling
# ═══════════════════════════════════════════════════════
class Sampler(threading.Thread):
    def __init__(self, store: DataStore):
        super().__init__(daemon=True, name='Sampler')
        self.store = store
        self._stop  = threading.Event()
        self._pnet  = None
        self._pdisk = None
        self._pt    = time.monotonic()
        self._nvidia_ok = None  # None=unknown, True/False

    def stop(self): self._stop.set()

    def run(self):
        psutil.cpu_percent(percpu=True)
        try:
            self._pnet  = psutil.net_io_counters()
            self._pdisk = psutil.disk_io_counters()
        except Exception: pass
        while not self._stop.wait(1.0):
            try: self._sample()
            except Exception: pass

    def _try_nvidia(self):
        if self._nvidia_ok is False: return 0.0, 0.0
        try:
            flags = getattr(subprocess, 'CREATE_NO_WINDOW', 0)
            r = subprocess.run(
                ['nvidia-smi', '--query-gpu=temperature.gpu,utilization.gpu',
                 '--format=csv,noheader,nounits'],
                capture_output=True, text=True, timeout=2, creationflags=flags)
            if r.returncode == 0:
                self._nvidia_ok = True
                p = r.stdout.strip().split(',')
                return float(p[0].strip()), float(p[1].strip())
        except Exception:
            self._nvidia_ok = False
        return 0.0, 0.0

    def _sample(self):
        now = time.monotonic()
        dt  = max(now - self._pt, 0.001)
        self._pt = now

        pc = psutil.cpu_percent(percpu=True)
        self.store.push_cpu(sum(pc) / len(pc) if pc else 0, pc)
        self.store.push('ram', psutil.virtual_memory().percent)

        gt, gu = self._try_nvidia()
        self.store.push('gpu_t', gt)
        self.store.push('gpu_u', gu)

        try:
            net = psutil.net_io_counters()
            if self._pnet:
                self.store.push('net_rx', max(0, (net.bytes_recv - self._pnet.bytes_recv) / dt / 1024))
                self.store.push('net_tx', max(0, (net.bytes_sent - self._pnet.bytes_sent) / dt / 1024))
            self._pnet = net
        except Exception: pass

        try:
            disk = psutil.disk_io_counters()
            if disk and self._pdisk:
                self.store.push('disk_r', max(0, (disk.read_bytes  - self._pdisk.read_bytes)  / dt / 1048576))
                self.store.push('disk_w', max(0, (disk.write_bytes - self._pdisk.write_bytes) / dt / 1048576))
            self._pdisk = disk
        except Exception: pass

# ═══════════════════════════════════════════════════════
#  HardwareCollector — detailed WMI + psutil info
# ═══════════════════════════════════════════════════════
class HardwareCollector:
    def __init__(self):
        self._w = self._wh = None
        self._static: dict = {}   # cache MB/BIOS/OS (se schimba doar la reboot)
        if WMI_OK:
            try: self._w  = _wmi_lib.WMI()
            except Exception: pass
            try: self._wh = _wmi_lib.WMI(namespace="root\\wmi")
            except Exception: pass

    def collect(self):
        import concurrent.futures as cf

        # Submite imediat toate task-urile pure PS (subprocess = thread-safe)
        # Ele ruleaza in paralel in timp ce WMI/psutil ruleaza secvential
        with cf.ThreadPoolExecutor(max_workers=9) as ex:
            f_security    = ex.submit(self._security)
            f_perf_extras = ex.submit(self._perf_extras)
            f_smart_ps    = ex.submit(self._smart_ps)
            f_rapl        = ex.submit(self._power_rapl)
            f_power_plan  = ex.submit(self._power_plan)
            f_vram_reg    = ex.submit(self._gpu_vram_registry)
            f_net_ps      = ex.submit(self._network_ps_extra)
            f_nvidia      = ex.submit(self._gpu_nvidia)
            f_lhm_temps   = ex.submit(self._gpu_lhm_temps)

            # WMI + psutil — secvential, pe thread-ul apelant (COM-safe)
            mem   = self._memory()
            sys_d = self._system()
            fans  = self._fans()

            # Colecteaza rezultatele PS (asteptate exact cat e nevoie)
            vram_map    = f_vram_reg.result()
            nvidia      = f_nvidia.result()
            lhm_temps   = f_lhm_temps.result() if not nvidia else {}
            gpu         = self._gpu(vram_map=vram_map, nvidia=nvidia, lhm_temps=lhm_temps)

            stor = self._storage(ps_data=f_smart_ps.result())
            psu  = self._psu(rapl=f_rapl.result(), power_plan=f_power_plan.result())
            net  = self._network(ps_extra=f_net_ps.result())

            security    = f_security.result()
            perf_extras = f_perf_extras.result()

        return {
            'storage':     stor,
            'memory':      mem,
            'gpu':         gpu,
            'system':      sys_d,
            'network':     net,
            'psu':         psu,
            'fans':        fans,
            'security':    security,
            'perf_extras': perf_extras,
            'ts':          datetime.now().strftime('%d.%m.%Y %H:%M:%S'),
            'hostname':    platform.node(),
        }

    # ── SMART via PowerShell (Get-PhysicalDisk + Get-StorageReliabilityCounter) ──
    def _smart_ps(self):
        ps = (
            "try{"
            "$out=Get-PhysicalDisk|%{"
            "$d=$_;$r=$null;"
            "try{$r=$d|Get-StorageReliabilityCounter -EA SilentlyContinue}catch{};"
            "[PSCustomObject]@{"
            "N=$d.FriendlyName;M=[string]$d.MediaType;H=[string]$d.HealthStatus;"
            "B=[string]$d.BusType;S=$d.Size;"
            "T=if($r-ne$null-and$r.Temperature-gt0){$r.Temperature}else{$null};"
            "W=if($r-ne$null-and$null-ne$r.Wear){$r.Wear}else{$null};"
            "P=if($r-ne$null-and$r.PowerOnHours-gt0){$r.PowerOnHours}else{$null};"
            "R=if($r-ne$null){$r.ReadErrorsUncorrected}else{$null};"
            "E=if($r-ne$null){$r.WriteErrorsUncorrected}else{$null}"
            "}"
            "};"
            "@($out)|ConvertTo-Json -Compress"
            "}catch{'[]'}"
        )
        raw = _run_ps(ps, timeout=12)
        if not raw:
            return []
        try:
            data = json.loads(raw)
            if isinstance(data, dict): data = [data]
            return data if isinstance(data, list) else []
        except Exception:
            return []

    # ── Storage ───────────────────────────────────────
    def _storage(self, ps_data=None):
        if ps_data is None:
            ps_data = self._smart_ps()

        def _find_ps(model, size):
            ml = model.lower()
            for pd in ps_data:
                fn = (pd.get('N') or '').lower()
                if fn and (fn in ml or ml in fn):
                    return pd
                if len(set(ml.split()) & set(fn.split())) >= 2:
                    return pd
                ps_sz = pd.get('S') or 0
                if size > 0 and ps_sz > 0 and abs(size - ps_sz) / size < 0.02:
                    return pd
            return None

        def _calc_health(smart_ok, status, pd, media):
            if smart_ok is False:
                return 'bad', 'SMART: Predictie esec!'
            ph   = (pd.get('H') or '').lower() if pd else ''
            temp = pd.get('T') if pd else None
            wear = pd.get('W') if pd else None
            errs = ((pd.get('R') or 0) + (pd.get('E') or 0)) if pd else 0
            if ph == 'unhealthy':
                h, hm = 'bad',     'Stare: Nesanatoasa'
            elif ph == 'warning':
                h, hm = 'warning', 'Stare: Avertisment'
            elif ph == 'healthy' or smart_ok is True:
                h, hm = 'good',    'SMART: OK'
            elif status.upper() == 'OK':
                h, hm = 'good',    'Status: OK'
            elif status:
                h, hm = 'warning', f'Status: {status}'
            else:
                h, hm = 'unknown', 'Necunoscut'
            if errs > 0 and h == 'good':
                h, hm = 'warning', f'Erori necorectate: {errs}'
            if temp is not None:
                is_ssd = any(k in media.lower() for k in ('ssd', 'solid', 'nvme'))
                th = health_of(temp, 'ssd_temp' if is_ssd else 'hdd_temp')
                if th == 'bad':
                    h, hm = 'bad',     f'Temperatura critica: {temp}°C'
                elif th == 'warning' and h == 'good':
                    h, hm = 'warning', f'Temperatura ridicata: {temp}°C'
            if wear is not None and 0 <= wear <= 100:
                if wear < 10 and h != 'bad':
                    h, hm = 'bad',     f'Uzura critica SSD: {wear}% ramas'
                elif wear < 20 and h == 'good':
                    h, hm = 'warning', f'Uzura SSD: {wear}% ramas'
            return h, hm

        disks = []
        if self._w:
            try:
                for d in self._w.Win32_DiskDrive():
                    size   = int(d.Size or 0) if d.Size else 0
                    status = (d.Status or '').strip()
                    model  = (d.Model or 'Necunoscut').strip()
                    smart_ok = None
                    try:
                        if self._wh:
                            for s in self._wh.MSStorageDriver_FailurePredictStatus():
                                inst = (s.InstanceName or '').upper()
                                if f"PHYSICALDRIVE{d.Index}" in inst or f"HARDDISK{d.Index}" in inst:
                                    smart_ok = not s.PredictFailure; break
                    except Exception: pass
                    pd    = _find_ps(model, size)
                    media = (pd.get('M') or d.MediaType or '').strip() if pd else (d.MediaType or '').strip()
                    h, hm = _calc_health(smart_ok, status, pd, media)
                    disks.append({
                        'model':     model,
                        'size':      size,
                        'iface':     (d.InterfaceType or 'N/A').strip(),
                        'media':     media or 'N/A',
                        'serial':    (d.SerialNumber or 'N/A').strip(),
                        'health':    h, 'hmsg': hm,
                        'temp':      pd.get('T')  if pd else None,
                        'wear':      pd.get('W')  if pd else None,
                        'hours':     pd.get('P')  if pd else None,
                        'read_err':  pd.get('R')  if pd else None,
                        'write_err': pd.get('E')  if pd else None,
                        'bus_type':  (pd.get('B') or '').strip() or None if pd else None,
                        'ps_health': (pd.get('H') or '').strip() or None if pd else None,
                    })
            except Exception: pass

        # Fallback to PS-only data if WMI returned nothing
        if not disks:
            for pd in ps_data:
                mt  = (pd.get('M') or '').strip()
                ph  = (pd.get('H') or '').lower()
                h   = 'good' if ph == 'healthy' else ('warning' if ph == 'warning' else ('bad' if ph == 'unhealthy' else 'unknown'))
                hm  = f'Stare: {pd.get("H") or "Necunoscut"}'
                temp = pd.get('T')
                if temp is not None:
                    is_ssd = any(k in mt.lower() for k in ('ssd', 'solid')) or 'nvme' in (pd.get('B') or '').lower()
                    th = health_of(temp, 'ssd_temp' if is_ssd else 'hdd_temp')
                    if th == 'bad':
                        h, hm = 'bad',     f'Temperatura critica: {temp}°C'
                    elif th == 'warning' and h == 'good':
                        h, hm = 'warning', f'Temperatura ridicata: {temp}°C'
                disks.append({
                    'model':     (pd.get('N') or 'Necunoscut').strip(),
                    'size':      pd.get('S') or 0,
                    'iface':     (pd.get('B') or 'N/A').strip(),
                    'media':     mt or 'N/A',
                    'serial':    'N/A',
                    'health':    h, 'hmsg': hm,
                    'temp':      temp,
                    'wear':      pd.get('W'),
                    'hours':     pd.get('P'),
                    'read_err':  pd.get('R'),
                    'write_err': pd.get('E'),
                    'bus_type':  (pd.get('B') or '').strip() or None,
                    'ps_health': pd.get('H'),
                })

        parts = []
        try:
            for p in psutil.disk_partitions(all=False):
                try:
                    u = psutil.disk_usage(p.mountpoint)
                    parts.append({'mp': p.mountpoint, 'fs': p.fstype,
                                  'total': u.total, 'used': u.used,
                                  'free': u.free, 'pct': u.percent})
                except Exception: pass
        except Exception: pass
        return {'disks': disks, 'parts': parts}

    # ── Memory ────────────────────────────────────────
    def _memory(self):
        vm, sm = psutil.virtual_memory(), psutil.swap_memory()
        slots = []
        if self._w:
            try:
                for m in self._w.Win32_PhysicalMemory():
                    cap = int(m.Capacity or 0) if m.Capacity else 0
                    mk  = str(m.SMBIOSMemoryType or m.MemoryType or '0')
                    slots.append({
                        'slot':  (m.BankLabel or m.DeviceLocator or 'N/A').strip(),
                        'cap':   cap,
                        'speed': m.Speed or 'N/A',
                        'type':  MEM_TYPE.get(mk, f'DDR (cod {mk})'),
                        'mfr':   (m.Manufacturer or 'N/A').strip(),
                        'part':  (m.PartNumber or '').strip() or 'N/A',
                    })
            except Exception: pass
        return {'total': vm.total, 'used': vm.used, 'available': vm.available,
                'pct': vm.percent, 'swap_total': sm.total, 'swap_used': sm.used,
                'swap_pct': sm.percent, 'slots': slots}

    # ── GPU ───────────────────────────────────────────
    _GPU_SKIP = ('remote desktop', 'virtual', 'parsec', 'teamviewer', 'vnc', 'rdp')

    def _gpu(self, vram_map=None, nvidia=None, lhm_temps=None):
        gpus = self._gpu_wmi()
        if not gpus:
            gpus = self._gpu_ps()
        # Corectie VRAM > 4 GB via registry
        if gpus:
            if vram_map is None:
                vram_map = self._gpu_vram_registry()
            for g in gpus:
                nl = g['name'].lower()
                for rn, rv in vram_map.items():
                    if rn.lower() in nl or nl in rn.lower():
                        if rv > g['vram']:
                            g['vram'] = rv
                        break
        if nvidia is None:
            nvidia = self._gpu_nvidia()
        if lhm_temps is None:
            lhm_temps = self._gpu_lhm_temps() if not nvidia else {}
        return {'gpus': gpus, 'nvidia': nvidia, 'lhm_temps': lhm_temps}

    def _gpu_wmi(self):
        gpus = []
        if not self._w: return gpus
        try:
            for g in self._w.Win32_VideoController():
                name = (g.Name or '').strip()
                if not name or any(k in name.lower() for k in self._GPU_SKIP):
                    continue
                vram = int(g.AdapterRAM or 0)
                if vram < 0: vram = 4 * 1024**3   # DWORD overflow = >4GB
                st = (g.Status or 'Unknown').strip()
                dd = str(g.DriverDate or '')
                if len(dd) >= 8: dd = f"{dd[:4]}-{dd[4:6]}-{dd[6:8]}"
                gpus.append({
                    'name':   name,
                    'vram':   vram,
                    'driver': (g.DriverVersion or 'N/A').strip(),
                    'dd':     dd or 'N/A',
                    'res':    f"{g.CurrentHorizontalResolution or 0}x{g.CurrentVerticalResolution or 0}",
                    'hz':     g.CurrentRefreshRate or 0,
                    'status': st,
                    'health': 'good' if st.upper() == 'OK' else 'warning',
                    'source': 'WMI',
                })
        except Exception: pass
        return gpus

    def _gpu_ps(self):
        """Fallback: PowerShell CIM — mai fiabil pe sisteme cu WMI restrictionat."""
        ps = (
            "try{@(Get-CimInstance Win32_VideoController -EA Stop)|"
            "Where-Object{$_.Name}|ForEach-Object{"
            "[PSCustomObject]@{N=$_.Name;V=[long]($_.AdapterRAM -as [long]);"
            "Dr=$_.DriverVersion;Dd=$_.DriverDate;St=$_.Status;"
            "Rx=$_.CurrentHorizontalResolution;Ry=$_.CurrentVerticalResolution;Hz=$_.CurrentRefreshRate"
            "}}|ConvertTo-Json -Compress}catch{'[]'}"
        )
        raw  = _run_ps(ps, timeout=10)
        gpus = []
        try:
            items = json.loads(raw)
            if isinstance(items, dict): items = [items]
            for g in (items or []):
                name = (g.get('N') or '').strip()
                if not name or any(k in name.lower() for k in self._GPU_SKIP):
                    continue
                vram = int(g.get('V') or 0)
                if vram < 0: vram = 4 * 1024**3
                raw_dd = str(g.get('Dd') or '')
                dd = raw_dd[:10] if len(raw_dd) >= 10 else raw_dd
                st = (g.get('St') or 'Unknown').strip()
                gpus.append({
                    'name':   name,
                    'vram':   vram,
                    'driver': (g.get('Dr') or 'N/A').strip(),
                    'dd':     dd or 'N/A',
                    'res':    f"{g.get('Rx') or 0}x{g.get('Ry') or 0}",
                    'hz':     g.get('Hz') or 0,
                    'status': st,
                    'health': 'good' if st.upper() == 'OK' else 'warning',
                    'source': 'PS',
                })
        except Exception: pass
        return gpus

    def _gpu_vram_registry(self):
        """Citeste VRAM dedicat real din registry (HardwareInformation.qwMemorySize)."""
        ps = (
            "try{"
            "$p='HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Class\\{4d36e968-e325-11ce-bfc1-08002be10318}';"
            "$r=@(Get-ChildItem $p -EA SilentlyContinue|ForEach-Object{"
            "$k=$_.PSPath;"
            "$nm=(Get-ItemProperty $k -Name 'DriverDesc' -EA SilentlyContinue).DriverDesc;"
            "$mem=(Get-ItemProperty $k -Name 'HardwareInformation.qwMemorySize' -EA SilentlyContinue).'HardwareInformation.qwMemorySize';"
            "if($nm -and $mem -gt 0){[PSCustomObject]@{N=$nm;M=[long]$mem}}"
            "});"
            "$r|ConvertTo-Json -Compress}catch{'[]'}"
        )
        result = {}
        raw = _run_ps(ps, timeout=8)
        try:
            items = json.loads(raw)
            if isinstance(items, dict): items = [items]
            for x in (items or []):
                nm  = (x.get('N') or '').strip()
                mem = int(x.get('M') or 0)
                if nm and mem > 0:
                    result[nm] = mem
        except Exception: pass
        return result

    def _gpu_nvidia(self):
        nvidia = []
        try:
            flags = getattr(subprocess, 'CREATE_NO_WINDOW', 0)
            r = subprocess.run(
                ['nvidia-smi',
                 '--query-gpu=name,temperature.gpu,utilization.gpu,utilization.memory,'
                 'memory.total,memory.used,power.draw,power.limit,clocks.current.graphics',
                 '--format=csv,noheader,nounits'],
                capture_output=True, text=True, timeout=5, creationflags=flags)
            if r.returncode == 0:
                for line in r.stdout.strip().split('\n'):
                    p = [x.strip() for x in line.split(',')]
                    if len(p) >= 6:
                        nvidia.append({'name': p[0], 'temp': p[1], 'gpu_u': p[2],
                                       'mem_u': p[3], 'mem_tot': p[4], 'mem_used': p[5],
                                       'power':  p[6] if len(p) > 6 else 'N/A',
                                       'plimit': p[7] if len(p) > 7 else 'N/A',
                                       'clk':    p[8] if len(p) > 8 else 'N/A'})
        except Exception: pass
        return nvidia

    def _gpu_lhm_temps(self):
        """Temperaturi GPU AMD/Intel din LHM/OHM (cand nvidia-smi nu e disponibil)."""
        temps = {}
        if not WMI_OK: return temps
        for ns in ('root\\LibreHardwareMonitor', 'root\\OpenHardwareMonitor'):
            try:
                w = _wmi_lib.WMI(namespace=ns)
                for s in w.Sensor():
                    if (s.SensorType or '').lower() != 'temperature' or not s.Value:
                        continue
                    nm  = (s.Name or '').lower()
                    par = (getattr(s, 'Parent', '') or '').lower()
                    if 'gpu' in nm or 'gpu' in par:
                        key = s.Name or 'GPU'
                        temps[key] = round(float(s.Value), 1)
                if temps: break
            except Exception: pass
        return temps

    # ── System ────────────────────────────────────────
    def _system(self):
        data = {
            'cpu': {'name': platform.processor(),
                    'pc': psutil.cpu_count(logical=False),
                    'lc': psutil.cpu_count(logical=True),
                    'usage': psutil.cpu_percent(interval=0.2)},
            'freq': {}, 'mb': {}, 'bios': {}, 'os': {},
            'uptime': '', 'boot': '', 'temps': [],
        }
        try:
            f = psutil.cpu_freq()
            if f: data['freq'] = {'cur': f.current, 'max': f.max}
        except Exception: pass
        try:
            bt = psutil.boot_time()
            up = time.time() - bt
            h, rem = divmod(int(up), 3600); m, _ = divmod(rem, 60)
            data['uptime'] = f"{h}h {m}min"
            data['boot']   = datetime.fromtimestamp(bt).strftime('%d.%m.%Y %H:%M')
        except Exception: pass
        if self._w:
            try:
                for c in self._w.Win32_Processor():
                    data['cpu']['name']   = c.Name.strip()
                    data['cpu']['socket'] = c.SocketDesignation or 'N/A'
                    break
            except Exception: pass
            # MB / BIOS / OS sunt statice — interogare o singura data
            if not self._static:
                try:
                    for mb in self._w.Win32_BaseBoard():
                        self._static['mb'] = {
                            'mfr':    (mb.Manufacturer or 'N/A').strip(),
                            'model':  (mb.Product      or 'N/A').strip(),
                            'ver':    (mb.Version      or 'N/A').strip(),
                            'serial': (mb.SerialNumber or 'N/A').strip()}; break
                except Exception: pass
                try:
                    for b in self._w.Win32_BIOS():
                        rd = str(b.ReleaseDate or '')
                        if len(rd) >= 8: rd = f"{rd[:4]}-{rd[4:6]}-{rd[6:8]}"
                        self._static['bios'] = {
                            'mfr':  (b.Manufacturer or 'N/A').strip(),
                            'ver':  (b.SMBIOSBIOSVersion or b.Version or 'N/A').strip(),
                            'date': rd or 'N/A'}; break
                except Exception: pass
                try:
                    for o in self._w.Win32_OperatingSystem():
                        self._static['os'] = {
                            'name':    (o.Caption          or 'N/A').strip(),
                            'ver':     (o.Version          or 'N/A').strip(),
                            'build':   (o.BuildNumber      or 'N/A').strip(),
                            'arch':    (o.OSArchitecture   or 'N/A').strip(),
                            'install': str(o.InstallDate   or '')[:10]}; break
                except Exception: pass
            data['mb']   = self._static.get('mb',   {})
            data['bios'] = self._static.get('bios', {})
            data['os']   = self._static.get('os',   {})
        try:
            if self._wh:
                for t in self._wh.MSAcpi_ThermalZoneTemperature():
                    raw_t = round((t.CurrentTemperature / 10.0) - 273.15, 1)
                    if -20 < raw_t < 150:   # sanity check
                        data['temps'].append(raw_t)
        except Exception: pass
        return data

    # ── Network ───────────────────────────────────────
    def _network_ps_extra(self):
        """Gateway, DNS, internet, latenta — doar PowerShell (thread-safe)."""
        gateways = []
        raw = _run_ps(
            "try{@(Get-NetIPConfiguration|Where-Object{$_.IPv4DefaultGateway}|ForEach-Object{"
            "[PSCustomObject]@{I=$_.InterfaceAlias;G=$_.IPv4DefaultGateway.NextHop;"
            "D=($_.DNSServer.ServerAddresses-join', ')}})|ConvertTo-Json -Compress}catch{'[]'}",
            timeout=8)
        try:
            gw = json.loads(raw)
            if isinstance(gw, dict): gw = [gw]
            gateways = [{'iface': x.get('I','?'), 'gateway': x.get('G','?'),
                         'dns': x.get('D','N/A')} for x in (gw or [])]
        except Exception: pass

        internet = False
        try:
            s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            s2.settimeout(3)
            s2.connect(('8.8.8.8', 53))
            s2.close()
            internet = True
        except Exception: pass

        latency_ms = -1
        if internet:
            raw = _run_ps(
                "try{$r=Test-Connection -ComputerName 8.8.8.8 -Count 3 -EA SilentlyContinue;"
                "if($r){[int]($r|Where-Object{$_.ResponseTime-gt 0}"
                "|Measure-Object ResponseTime -Average).Average}"
                "else{-1}}catch{-1}", timeout=12)
            try: latency_ms = int(raw.strip())
            except Exception: pass

        return {'gateways': gateways, 'internet': internet, 'latency_ms': latency_ms}

    def _network(self, ps_extra=None):
        adapters = []
        if self._w:
            try:
                for nic in self._w.Win32_NetworkAdapter(PhysicalAdapter=True):
                    if not nic.NetEnabled: continue
                    speed = int(nic.Speed or 0) if nic.Speed else 0
                    adapters.append({'name': (nic.Name or '').strip(),
                                     'mac':   nic.MACAddress or 'N/A',
                                     'speed': speed,
                                     'type':  nic.AdapterType or 'N/A'})
            except Exception: pass
        addrs = {}
        try:
            for iface, lst in psutil.net_if_addrs().items():
                ips = [a.address for a in lst if a.family.name == 'AF_INET']
                if ips: addrs[iface] = ips
        except Exception: pass
        stats = {}
        try:
            for iface, s in psutil.net_if_stats().items():
                stats[iface] = {'speed': s.speed, 'up': s.isup}
        except Exception: pass

        if ps_extra is None:
            ps_extra = self._network_ps_extra()

        return {'adapters': adapters, 'addrs': addrs, 'stats': stats, **ps_extra}

    # ── Security ─────────────────────────────────────
    def _security(self):
        ps = (
            "try{"
            "$d=@{};$fw=@();$bl=@();$act='Necunoscut';$rdp='Necunoscut';$adm=@();$upd=@();"
            "try{$s=Get-MpComputerStatus -EA Stop;"
            "$d=@{E=[bool]$s.AMServiceEnabled;R=[bool]$s.RealTimeProtectionEnabled;"
            "A=[int]$s.AntivirusSignatureAge;L=[string]$s.QuickScanEndTime;V=$s.AMProductVersion}}catch{};"
            "try{$fw=@(Get-NetFirewallProfile|Select-Object Name,Enabled)}catch{};"
            "try{$bl=@(Get-BitLockerVolume|Select-Object MountPoint,VolumeStatus,ProtectionStatus)}catch{};"
            "try{$l=(Get-CimInstance SoftwareLicensingProduct -Filter \"Name like 'Windows%' and PartialProductKey is not null\" -EA Stop|Select-Object -First 1);"
            "switch($l.LicenseStatus){1{$act='Activat'}0{$act='Nelicentiat'}default{$act=\"Status $($l.LicenseStatus)\"}}}catch{};"
            "try{$v=(Get-ItemProperty 'HKLM:\\System\\CurrentControlSet\\Control\\Terminal Server' -Name fDenyTSConnections -EA Stop).fDenyTSConnections;"
            "$rdp=if($v-eq 0){'Activat'}else{'Dezactivat'}}catch{};"
            "try{$adm=@(Get-LocalGroupMember Administrators -EA Stop|Select-Object Name,PrincipalSource)}catch{};"
            "try{$upd=@(Get-HotFix|Sort-Object InstalledOn -Desc|Select-Object -First 5 HotFixID,Description,InstalledOn)}catch{};"
            "[PSCustomObject]@{D=$d;F=$fw;B=$bl;A=$act;P=$rdp;M=$adm;U=$upd}|ConvertTo-Json -Depth 4 -Compress"
            "}catch{'{}' }"
        )
        raw = _run_ps(ps, timeout=35)
        res = {
            'defender': {'available': False}, 'firewall': [], 'bitlocker': [],
            'activation': 'Necunoscut', 'rdp': 'Necunoscut', 'admins': [], 'updates': [],
        }
        try:
            r = json.loads(raw)
            d = r.get('D') or {}
            if d:
                res['defender'] = {
                    'available': True,
                    'enabled':        bool(d.get('E')),
                    'rt_protection':  bool(d.get('R')),
                    'sig_age_days':   int(d.get('A') or 0),
                    'last_scan':      str(d.get('L') or '')[:16] or 'N/A',
                    'version':        d.get('V') or 'N/A',
                }
            fw = r.get('F') or []
            if isinstance(fw, dict): fw = [fw]
            res['firewall'] = [{'name': x.get('Name','?'), 'enabled': bool(x.get('Enabled'))}
                                for x in fw if x]
            bl = r.get('B') or []
            if isinstance(bl, dict): bl = [bl]
            res['bitlocker'] = [
                {'mount': x.get('MountPoint','?'),
                 'status': str(x.get('VolumeStatus','?')),
                 'protection': str(x.get('ProtectionStatus','?'))}
                for x in bl if x
            ]
            res['activation'] = str(r.get('A') or 'Necunoscut')
            res['rdp']        = str(r.get('P') or 'Necunoscut')
            adm = r.get('M') or []
            if isinstance(adm, dict): adm = [adm]
            res['admins'] = [
                {'name': str(x.get('Name','?')).split('\\')[-1],
                 'source': str(x.get('PrincipalSource', 0))}
                for x in adm if x
            ]
            upd = r.get('U') or []
            if isinstance(upd, dict): upd = [upd]
            res['updates'] = [
                {'id': x.get('HotFixID','?'), 'desc': x.get('Description','?'),
                 'date': str(x.get('InstalledOn',''))[:10]}
                for x in upd if x
            ]
        except Exception: pass
        return res

    # ── Performance extras ────────────────────────────
    def _perf_extras(self):
        ps = (
            "try{"
            "$st=@();$bt='';$ev=@();"
            "try{$st=@(Get-CimInstance Win32_StartupCommand|Select-Object Name,Command,Location,User)}catch{};"
            "try{"
            "$e=Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-Diagnostics-Performance/Operational';Id=100}"
            " -MaxEvents 1 -EA SilentlyContinue;"
            "if($e){$xml=[xml]$e.ToXml();"
            "$bt=($xml.Event.EventData.Data|Where-Object{$_.Name-eq'BootTime'}|Select-Object -Expand '#text')}"
            "}catch{};"
            "try{"
            "$ev=@(Get-WinEvent -FilterHashtable @{LogName='System';Level=1,2;StartTime=(Get-Date).AddDays(-7)}"
            " -MaxEvents 5 -EA SilentlyContinue|Select-Object TimeCreated,Id,"
            "@{N='M';E={$_.Message.Substring(0,[Math]::Min(100,$_.Message.Length))}})"
            "}catch{};"
            "[PSCustomObject]@{S=$st;B=$bt;E=$ev}|ConvertTo-Json -Depth 3 -Compress"
            "}catch{'{}' }"
        )
        raw = _run_ps(ps, timeout=20)
        res = {'startup': [], 'boot_ms': None, 'events': []}
        try:
            r = json.loads(raw)
            st = r.get('S') or []
            if isinstance(st, dict): st = [st]
            res['startup'] = [
                {'name': x.get('Name','?'),
                 'command': (x.get('Command') or '')[:80],
                 'location': x.get('Location','?'),
                 'user': x.get('User','?')}
                for x in st if x
            ]
            bt = str(r.get('B') or '').strip()
            if bt.lstrip('-').isdigit(): res['boot_ms'] = int(bt)
            ev = r.get('E') or []
            if isinstance(ev, dict): ev = [ev]
            res['events'] = [
                {'time': str(x.get('TimeCreated',''))[:16],
                 'id': x.get('Id', 0),
                 'msg': str(x.get('M') or '').replace('\n',' ')[:100]}
                for x in ev if x
            ]
        except Exception: pass
        return res

    # ── PSU + Battery Health ──────────────────────────
    def _psu(self, rapl=None, power_plan=None):
        bat = psutil.sensors_battery()
        if bat:
            result = {
                'type': 'laptop', 'pct': bat.percent,
                'plugged': bat.power_plugged, 'secs': bat.secsleft,
                'design_cap': None, 'full_cap': None, 'health_pct': None,
            }
            if self._wh:
                design_cap = full_cap = None
                try:
                    for b in self._wh.BatteryStaticData():
                        if b.DesignedCapacity:
                            design_cap = int(b.DesignedCapacity); break
                except Exception: pass
                try:
                    for b in self._wh.BatteryFullChargedCapacity():
                        if b.FullChargedCapacity:
                            full_cap = int(b.FullChargedCapacity); break
                except Exception: pass
                if design_cap and full_cap and design_cap > 0:
                    result['design_cap'] = design_cap
                    result['full_cap']   = full_cap
                    result['health_pct'] = round(min(100.0, full_cap / design_cap * 100), 1)
        else:
            result = {'type': 'desktop'}

        # CPU VCore via Win32_Processor.CurrentVoltage (zecimi de volt)
        vcores = []
        if self._w:
            try:
                for p in self._w.Win32_Processor():
                    cv = getattr(p, 'CurrentVoltage', None)
                    if cv and int(cv) > 0:
                        vcores.append({
                            'name':    (p.Name or 'CPU').strip()[:50],
                            'voltage': round(int(cv) / 10.0, 2),
                        })
            except Exception: pass
        result['vcores']     = vcores
        result['rapl']       = rapl       if rapl       is not None else self._power_rapl()
        result['power_plan'] = power_plan if power_plan is not None else self._power_plan()
        return result

    def _power_rapl(self):
        """Consum energie via Windows Performance Counters (RAPL — Intel/AMD Zen3+)."""
        ps = (
            "try{"
            "$c=Get-Counter '\\Power Meter(*)\\Power' -EA SilentlyContinue;"
            "if($c){@($c.CounterSamples|Where-Object{$_.CookedValue -gt 0}|"
            "ForEach-Object{[PSCustomObject]@{"
            "N=$_.InstanceName;W=[math]::Round($_.CookedValue,1)}})"
            "|ConvertTo-Json -Compress}"
            "else{'[]'}}catch{'[]'}"
        )
        raw = _run_ps(ps, timeout=10)
        result = []
        try:
            items = json.loads(raw)
            if isinstance(items, dict): items = [items]
            for x in (items or []):
                nm = (x.get('N') or '').strip()
                w  = float(x.get('W') or 0)
                if nm and w > 0:
                    result.append({'name': nm, 'watts': w})
        except Exception: pass
        return result

    def _power_plan(self):
        """Returneaza planul de alimentare activ Windows."""
        ps = (
            "try{"
            "$p=Get-WmiObject -Namespace root\\cimv2\\power -Class Win32_PowerPlan"
            " -Filter \"IsActive=True\" -EA Stop|Select-Object -First 1;"
            "$p.ElementName"
            "}catch{"
            "try{(powercfg /getactivescheme 2>$null)"
            " -replace '.*\\((.+)\\)\\s*$','$1'}catch{'Necunoscut'}}"
        )
        raw = _run_ps(ps, timeout=6)
        return raw.strip() or 'Necunoscut'

    # ── Fan speeds ────────────────────────────────────
    def _fans(self):
        fans = []
        if WMI_OK:
            # Try LibreHardwareMonitor then OpenHardwareMonitor (most reliable)
            for ns in ('root\\LibreHardwareMonitor', 'root\\OpenHardwareMonitor'):
                try:
                    w = _wmi_lib.WMI(namespace=ns)
                    for s in w.Sensor():
                        if (s.SensorType or '').lower() == 'fan' and s.Value:
                            fans.append({
                                'name':   (s.Name or 'Fan').strip(),
                                'rpm':    int(float(s.Value)),
                                'parent': (getattr(s, 'Parent', '') or '').strip(),
                                'source': ns.split('\\')[-1],
                            })
                    if fans: break
                except Exception: pass
            # Fallback: Win32_Fan (often empty on modern systems)
            if not fans and self._w:
                try:
                    for f in self._w.Win32_Fan():
                        rpm = int(f.DesiredSpeed or 0)
                        fans.append({
                            'name':   (f.Name or 'Fan').strip(),
                            'rpm':    rpm,
                            'parent': '',
                            'source': 'WMI',
                        })
                except Exception: pass
        return fans

# ═══════════════════════════════════════════════════════
#  HTML Exporter
# ═══════════════════════════════════════════════════════
class HTMLExporter:
    def export(self, data: dict, store: DataStore, path: str):
        ts    = data.get('ts', '')
        mem   = data.get('memory', {})
        sys_d = data.get('system', {})
        gpu_d = data.get('gpu', {})
        stor  = data.get('storage', {})

        cpu_now = list(store.cpu)[-1] if store.cpu else 0
        ram_now = mem.get('pct', 0)

        labels = {'good': 'OK', 'warning': 'Atentie!', 'bad': 'Problema!', 'unknown': 'Info'}

        def card(title, health, body):
            lbl = labels.get(health, health)
            return (f'<div class="card {health}"><h3>{title} '
                    f'<span class="badge {health}">{lbl}</span></h3>{body}</div>\n')

        def row(k, v): return f'<tr><td>{k}</td><td>{v}</td></tr>'
        def bar(pct, h): return (f'<div class="bar-bg"><div class="bar-fill" '
                                  f'style="width:{pct:.0f}%;background:{HC[h]};"></div></div>')

        cpu_h = health_of(cpu_now, 'cpu')
        ram_h = health_of(ram_now, 'ram')
        cpu_info = sys_d.get('cpu', {})
        freq     = sys_d.get('freq', {})

        cards = ''
        cards += card('Procesor', cpu_h, f'<table>'
                      + row('Model', cpu_info.get('name', 'N/A'))
                      + row('Nuclee', f"{cpu_info.get('pc','?')} fizice / {cpu_info.get('lc','?')} logice")
                      + row('Frecventa max', f"{freq.get('max',0):.0f} MHz")
                      + row('Utilizare', f'{cpu_now:.0f}%')
                      + '</table>' + bar(cpu_now, cpu_h))

        cards += card('Memorie RAM', ram_h, f'<table>'
                      + row('Total', fmt_bytes(mem.get('total', 0)))
                      + row('Utilizat', f"{fmt_bytes(mem.get('used',0))} ({ram_now:.0f}%)")
                      + row('Disponibil', fmt_bytes(mem.get('available', 0)))
                      + '</table>' + bar(ram_now, ram_h))

        for g in gpu_d.get('gpus', []):
            cards += card(f"GPU: {g['name']}", g['health'], f'<table>'
                          + row('VRAM', fmt_bytes(g.get('vram', 0)))
                          + row('Driver', g.get('driver', 'N/A'))
                          + row('Rezolutie', g.get('res', 'N/A'))
                          + '</table>')

        for d in stor.get('disks', []):
            extra = ''
            if d.get('temp') is not None:
                extra += row('Temperatura', f"{d['temp']}°C")
            if d.get('hours') and d['hours'] > 0:
                extra += row('Ore functionare', f"{d['hours']:,}")
            if d.get('wear') is not None and 0 <= d['wear'] <= 100:
                extra += row('Viata ramasa SSD', f"{d['wear']}%")
            if d.get('read_err') is not None or d.get('write_err') is not None:
                errs = (d.get('read_err') or 0) + (d.get('write_err') or 0)
                extra += row('Erori necorectate', str(errs))
            cards += card(f"Disc: {d['model']}", d['health'], f'<table>'
                          + row('Dimensiune', fmt_bytes(d.get('size', 0)))
                          + row('Interfata', d.get('iface', 'N/A'))
                          + row('SMART', d.get('hmsg', 'N/A'))
                          + extra
                          + '</table>')

        # Partitions table
        parts_html = ''
        for p in stor.get('parts', []):
            ph = health_of(p['pct'], 'disk_pct')
            parts_html += (f"<tr><td>{p['mp']} ({p['fs']})</td>"
                           f"<td>{fmt_bytes(p['total'])}</td>"
                           f"<td>{fmt_bytes(p['used'])}</td>"
                           f"<td>{fmt_bytes(p['free'])}</td>"
                           f"<td><span class='badge {ph}'>{p['pct']:.1f}%</span></td></tr>")

        mb   = sys_d.get('mb', {})
        bios = sys_d.get('bios', {})
        os_d = sys_d.get('os', {})
        sys_cards = ''
        if mb:   sys_cards += card('Placa de baza', 'unknown', f'<table>'
                                   + row('Producator', mb.get('mfr','N/A'))
                                   + row('Model', mb.get('model','N/A'))
                                   + row('Serial', mb.get('serial','N/A')) + '</table>')
        if bios: sys_cards += card('BIOS/UEFI', 'unknown', f'<table>'
                                   + row('Versiune', bios.get('ver','N/A'))
                                   + row('Data', bios.get('date','N/A')) + '</table>')
        if os_d: sys_cards += card('Sistem de operare', 'unknown', f'<table>'
                                   + row('Nume', os_d.get('name','N/A'))
                                   + row('Build', os_d.get('build','N/A'))
                                   + row('Arhitectura', os_d.get('arch','N/A')) + '</table>')

        html = f"""<!DOCTYPE html>
<html lang="ro"><head><meta charset="UTF-8">
<title>PC Health Report — {ts}</title>
<style>
*{{box-sizing:border-box}}
body{{font-family:'Segoe UI',sans-serif;background:#0d1117;color:#c9d1d9;margin:0;padding:24px}}
h1{{color:#58a6ff;border-bottom:1px solid #30363d;padding-bottom:12px;margin:0 0 6px}}
h2{{color:#79c0ff;font-size:15px;margin:24px 0 10px}}
.sub{{color:#8b949e;font-size:12px;margin:0 0 20px}}
.grid{{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:14px;margin-bottom:24px}}
.card{{background:#161b22;border:1px solid #30363d;border-radius:10px;padding:16px}}
.card h3{{margin:0 0 12px;font-size:14px;display:flex;justify-content:space-between;align-items:center}}
.card.good{{border-color:#00C853}}.card.warning{{border-color:#FFB300}}.card.bad{{border-color:#F44336}}
.badge{{padding:2px 8px;border-radius:4px;font-size:11px;font-weight:700}}
.badge.good{{background:#00C853;color:#000}}.badge.warning{{background:#FFB300;color:#000}}
.badge.bad{{background:#F44336;color:#fff}}.badge.unknown{{background:#484f58;color:#fff}}
.bar-bg{{background:#21262d;border-radius:4px;height:6px;margin-top:10px}}
.bar-fill{{height:6px;border-radius:4px}}
table{{width:100%;border-collapse:collapse;font-size:13px}}
td{{padding:4px 6px;border-bottom:1px solid #21262d}}td:first-child{{color:#8b949e;width:42%}}
hr{{border:none;border-top:1px solid #21262d;margin:24px 0}}
.footer{{color:#484f58;font-size:11px;text-align:center;margin-top:24px}}
</style></head><body>
<h1>&#x1F4BB; PC Health Report</h1>
<p class="sub">Generat: {ts} &nbsp;|&nbsp; {os_d.get('name','Windows')} {os_d.get('arch','')}</p>
<h2>&#x1F4CA; Componente principale</h2>
<div class="grid">{cards}</div>
<h2>&#x1F4BE; Utilizare discuri</h2>
<table>
<tr style="color:#8b949e;font-size:12px"><td>Partitie</td><td>Total</td><td>Utilizat</td><td>Liber</td><td>%</td></tr>
{parts_html}
</table>
<h2>&#x2699;&#xFE0F; Informatii sistem</h2>
<div class="grid">{sys_cards}</div>
<hr>
<p class="footer">Generat de {TITLE} v{VERSION}</p>
</body></html>"""

        with open(path, 'w', encoding='utf-8') as f:
            f.write(html)

# ═══════════════════════════════════════════════════════
#  PDF Exporter
# ═══════════════════════════════════════════════════════
class PDFExporter:
    """Genereaza raport profesional A4 in format PDF (fpdf2)."""

    # ── Paleta culori (r, g, b) ───────────────────────
    _H_BG  = (10,  15,  30)   # banner header
    _H_FG  = (255, 255, 255)  # text alb header
    _H_SB  = (155, 185, 225)  # subtext header
    _S_BG  = (30,  58, 138)   # sectiune titlu fundal
    _S_FG  = (255, 255, 255)  # sectiune titlu text
    _T_BG  = (51,  65,  85)   # tabel header fundal
    _T_FG  = (255, 255, 255)  # tabel header text
    _R_A   = (255, 255, 255)  # rand impar (alb)
    _R_B   = (248, 250, 252)  # rand par (slate-50)
    _LBL   = (100, 116, 139)  # eticheta gri
    _TXT   = (15,  23,  42)   # text principal
    _BOR   = (226, 232, 240)  # border
    _C_OK  = (0,   160,  65)  # verde bun
    _C_WN  = (185, 110,   0)  # amber atentie
    _C_ER  = (195,  30,  30)  # rosu problema
    _C_NK  = (120, 130, 148)  # gri necunoscut
    _C_INF = (37,  99,  235)  # albastru info

    def _hc(self, h: str) -> tuple:
        return {'good': self._C_OK, 'warning': self._C_WN,
                'bad':  self._C_ER}.get(h, self._C_NK)

    def _hl(self, h: str) -> str:
        return {'good': 'OK', 'warning': 'Atentie', 'bad': 'Problema'}.get(h, '—')

    # ── Helpere layout ────────────────────────────────
    def _sec_hdr(self, title: str):
        p = self._pdf
        p.ln(5)
        p.set_fill_color(*self._S_BG)
        p.set_text_color(*self._S_FG)
        p.set_font(self._fn, 'B', 10)
        p.cell(self._W, 7, f'  {title}', fill=True,
               new_x='LMARGIN', new_y='NEXT')
        p.ln(1)

    def _tbl_hdr(self, labels: list, widths: list):
        p = self._pdf
        p.set_fill_color(*self._T_BG)
        p.set_text_color(*self._T_FG)
        p.set_font(self._fn, 'B', 8)
        for lbl, w in zip(labels, widths):
            p.cell(w, 6, f' {lbl}', fill=True)
        p.ln()

    def _tbl_row(self, cells: list, widths: list,
                 alt: bool = False, hl: str = ''):
        p = self._pdf
        p.set_fill_color(*(self._R_B if alt else self._R_A))
        p.set_font(self._fn, '', 8)
        for i, (txt, w) in enumerate(zip(cells, widths)):
            if hl and i == len(cells) - 1:
                p.set_text_color(*self._hc(hl))
                p.set_font(self._fn, 'B', 8)
                p.cell(w, 5.5, f' {txt}', fill=True)
                p.set_font(self._fn, '', 8)
            else:
                p.set_text_color(*self._TXT)
                p.cell(w, 5.5, f' {txt}', fill=True)
        p.ln()

    def _kv(self, label: str, value: str,
            alt: bool = False, hl: str = ''):
        p = self._pdf
        p.set_fill_color(*(self._R_B if alt else self._R_A))
        p.set_font(self._fn, '', 8.5)
        p.set_text_color(*self._LBL)
        p.cell(62, 5.5, f'  {label}', fill=True)
        if hl:
            p.set_text_color(*self._hc(hl))
            p.set_font(self._fn, 'B', 8.5)
        else:
            p.set_text_color(*self._TXT)
        p.cell(self._W - 62, 5.5, str(value)[:90], fill=True,
               new_x='LMARGIN', new_y='NEXT')

    # ── Sectiuni continut ─────────────────────────────
    def _draw_page_header(self, data: dict):
        p = self._pdf
        p.set_fill_color(*self._H_BG)
        p.rect(0, 0, 210, 38, 'F')
        p.set_xy(15, 9)
        p.set_font(self._fn, 'B', 22)
        p.set_text_color(*self._H_FG)
        p.cell(130, 10, 'PC Health Report')
        p.set_xy(15, 22)
        p.set_font(self._fn, 'B', 9)
        p.set_text_color(*self._H_SB)
        ts    = data.get('ts', '')
        os_n  = data.get('system', {}).get('os', {}).get('name', '')
        build = data.get('system', {}).get('os', {}).get('build', '')
        pieces = [p2 for p2 in [ts, os_n, f'Build {build}' if build else ''] if p2]
        p.cell(0, 5, '  |  '.join(pieces))
        p.set_xy(15, 42)

    def _draw_score(self, data: dict):
        p = self._pdf
        score, grade, cats = _calc_health_score(data)
        col  = self._hc(grade)
        gmap = {'good': 'BINE', 'warning': 'ATENTIE', 'bad': 'PROBLEMA'}
        glbl = gmap.get(grade, 'N/A')
        y0   = p.get_y()

        # Dreptunghi colorat cu scor
        p.set_fill_color(*col)
        p.rect(15, y0, 50, 28, 'F')
        p.set_xy(15, y0 + 3)
        p.set_font(self._fn, 'B', 28)
        p.set_text_color(255, 255, 255)
        p.cell(50, 12, str(score), align='C')
        p.set_xy(15, y0 + 15)
        p.set_font(self._fn, 'B', 8)
        p.cell(50, 8, f'/ 100  ·  {glbl}', align='C')

        # Categorie chips pe dreapta
        p.set_xy(70, y0 + 2)
        p.set_font(self._fn, 'B', 11)
        p.set_text_color(*self._TXT)
        p.cell(0, 7, 'Scor sanatate sistem')
        p.set_xy(70, y0 + 12)
        x_cur = 70
        for cname, cgot, cmax in cats:
            cpct  = round(cgot / cmax * 100) if cmax else 0
            ch    = 'good' if cpct >= 75 else ('warning' if cpct >= 50 else 'bad')
            chip_lbl = f'{cname} {cgot}/{cmax}'
            chip_w   = max(22, len(chip_lbl) * 2.1 + 4)
            if x_cur + chip_w > 192:
                p.set_xy(70, p.get_y() + 6)
                x_cur = 70
            p.set_xy(x_cur, p.get_y())
            p.set_fill_color(*self._hc(ch))
            p.set_text_color(255, 255, 255)
            p.set_font(self._fn, 'B', 7)
            p.cell(chip_w, 5, chip_lbl, fill=True, align='C')
            p.cell(2, 5, '')
            x_cur += chip_w + 2
        p.set_xy(15, y0 + 34)

    def _draw_overview(self, data: dict):
        self._sec_hdr('STARE RAPIDA COMPONENTE')
        p  = self._pdf
        items = self._health_items(data)
        hh = 5.5
        col_w = (self._W - 4) // 2
        half  = (len(items) + 1) // 2
        y0    = p.get_y()
        for i, (hlth, label, msg) in enumerate(items):
            ci = i // half
            ri = i  % half
            x  = 15 + ci * (col_w + 4)
            y  = y0 + ri * hh
            alt = ri % 2 == 1
            p.set_xy(x, y)
            p.set_fill_color(*(self._R_B if alt else self._R_A))
            p.set_text_color(*self._hc(hlth))
            p.set_font(self._fn, 'B', 8)
            p.cell(7, hh, '●', fill=True, align='C')
            p.set_text_color(*self._TXT)
            p.set_font(self._fn, '', 8)
            p.cell(col_w - 7, hh, f'{label}:  {msg}', fill=True)
        p.set_y(y0 + half * hh + 3)

    def _health_items(self, data: dict) -> list:
        items = []
        # CPU
        temps = data.get('system', {}).get('temps', [])
        avg_t = round(sum(temps) / len(temps)) if temps else None
        h = ('good'    if avg_t and avg_t < 70 else
             'warning' if avg_t and avg_t < 85 else
             'bad'     if avg_t else 'unknown')
        items.append((h, 'Procesor', f'{avg_t}°C' if avg_t else 'N/A'))
        # RAM
        mem = data.get('memory', {})
        pct = mem.get('pct', 0)
        h   = 'good' if pct < 70 else ('warning' if pct < 90 else 'bad')
        items.append((h, 'Memorie RAM',
                      f'{pct}%  ({fmt_bytes(mem.get("used",0))} / {fmt_bytes(mem.get("total",0))})'))
        # Stocare
        disks = data.get('storage', {}).get('disks', [])
        parts = data.get('storage', {}).get('parts', [])
        if disks:
            bad  = any(d['health'] == 'bad'     for d in disks)
            warn = any(d['health'] == 'warning'  for d in disks) or \
                   any(pt['pct'] > 90            for pt in parts)
            h = 'bad' if bad else ('warning' if warn else 'good')
            items.append((h, 'Stocare',
                          f'{len(disks)} disk(uri)' +
                          (', probleme SMART' if bad else ', atentie' if warn else ', OK')))
        # GPU
        gpus = data.get('gpu', {}).get('gpus', [])
        if gpus:
            g0   = gpus[0]
            nvs  = data.get('gpu', {}).get('nvidia', [])
            temp = int(nvs[0].get('temp', 0)) if nvs else None
            lhm  = data.get('gpu', {}).get('lhm_temps', {})
            if not temp and lhm:
                temp = int(next(iter(lhm.values())))
            h = ('good' if temp and temp < 75 else
                 'warning' if temp and temp < 90 else
                 'bad' if temp else 'good')
            inf = g0.get('name', 'GPU')
            if temp: inf += f', {temp}°C'
            items.append((h, 'GPU', inf[:40]))
        # Internet
        online = data.get('network', {}).get('internet')
        lat    = data.get('network', {}).get('latency_ms', -1)
        if online is True:
            msg = 'Online'
            if lat and lat > 0: msg += f', {lat} ms'
            items.append(('good', 'Internet', msg))
        elif online is False:
            items.append(('bad', 'Internet', 'Offline'))
        # Defender
        df = data.get('security', {}).get('defender', {})
        if df.get('available'):
            h = ('good'    if df.get('enabled') and df.get('rt_protection') else
                 'warning' if df.get('enabled') else 'bad')
            items.append((h, 'Windows Defender',
                           'Activ, RT ON'  if h == 'good' else
                           'Activ, RT OFF' if df.get('enabled') else 'Dezactivat'))
        # Baterie
        psu = data.get('psu', {})
        if psu.get('type') == 'laptop':
            bpct  = psu.get('pct', 0)
            plugg = psu.get('plugged', False)
            h     = 'good' if bpct > 50 else ('warning' if bpct > 20 else 'bad')
            items.append((h, 'Baterie',
                           f'{bpct}%  {"(incarcare)" if plugg else "(baterie)"}'))
        return items

    def _draw_security(self, data: dict):
        sec = data.get('security', {})
        if not sec: return
        self._sec_hdr('SECURITATE')
        alt = False
        df  = sec.get('defender', {})
        if df.get('available'):
            h = ('good'    if df.get('enabled') and df.get('rt_protection') else
                 'warning' if df.get('enabled') else 'bad')
            self._kv('Windows Defender',
                     'Activ' if df.get('enabled') else 'Dezactivat', alt, h); alt = not alt
            if df.get('enabled'):
                rt = df.get('rt_protection')
                self._kv('  Protectie in timp real',
                         'Activata' if rt else 'Dezactivata', alt,
                         'good' if rt else 'bad'); alt = not alt
                sig = df.get('sig_age_days')
                if sig is not None:
                    sh = 'good' if sig <= 7 else ('warning' if sig <= 30 else 'bad')
                    self._kv('  Varsta semnatura (zile)', str(sig), alt, sh); alt = not alt
        for fw in sec.get('firewall', []):
            en = fw.get('enabled', False)
            self._kv(f"Firewall {fw.get('name','')}",
                     'Activ' if en else 'Dezactivat', alt,
                     'good' if en else 'bad'); alt = not alt
        act = sec.get('activation', '')
        if act:
            self._kv('Activare Windows', act, alt,
                     'good' if act == 'Activat' else 'bad'); alt = not alt
        rdp = sec.get('rdp', '')
        if rdp:
            self._kv('Remote Desktop', rdp, alt,
                     'warning' if rdp == 'Activat' else 'good'); alt = not alt
        for bv in sec.get('bitlocker', []):
            prot = bv.get('protection', '')
            self._kv(f"BitLocker {bv.get('mount','')}",
                     prot, alt,
                     'good' if 'On' in prot else 'warning'); alt = not alt
        upd = sec.get('updates', [])
        if upd:
            self._pdf.ln(2)
            self._pdf.set_font(self._fn, 'B', 8.5)
            self._pdf.set_text_color(*self._LBL)
            self._pdf.cell(0, 5, '  Ultimele actualizari Windows:',
                           new_x='LMARGIN', new_y='NEXT')
            self._tbl_hdr(['KB / ID', 'Descriere', 'Data'], [28, 112, 25])
            for i, u in enumerate(upd[:5]):
                self._tbl_row([u.get('id',''), u.get('desc','')[:55],
                               u.get('date','')[:10]],
                              [28, 112, 25], alt=i % 2 == 1)
        self._pdf.ln(2)

    def _draw_storage(self, data: dict):
        stor  = data.get('storage', {})
        disks = stor.get('disks', [])
        parts = stor.get('parts', [])
        if not disks and not parts: return
        self._sec_hdr('STOCARE')
        if disks:
            self._tbl_hdr(
                ['Model', 'Tip', 'Capacitate', 'Bus', 'SMART', 'Temp.', 'Ore'],
                [60, 14, 22, 14, 28, 16, 16])
            for i, d in enumerate(disks):
                smart = (d.get('smart_status') or
                         ('OK' if d.get('health') == 'good'
                          else 'Atentie' if d.get('health') == 'warning'
                          else 'Problema'))
                self._tbl_row([
                    (d.get('model') or 'N/A')[:30],
                    (d.get('media_type') or '')[:8],
                    d.get('size', '—'),
                    (d.get('bus') or '')[:7],
                    smart[:14],
                    f"{d.get('temperature','—')}°C" if d.get('temperature') else '—',
                    str(d.get('hours_on', '—')),
                ], [60, 14, 22, 14, 28, 16, 16],
                   alt=i % 2 == 1, hl=d.get('health', 'unknown'))
        if parts:
            self._pdf.ln(3)
            self._pdf.set_font(self._fn, 'B', 8.5)
            self._pdf.set_text_color(*self._LBL)
            self._pdf.cell(0, 5, '  Partitii:',
                           new_x='LMARGIN', new_y='NEXT')
            self._tbl_hdr(['Punct montare', 'Total', 'Folosit', 'Liber', 'Ocupat'],
                          [64, 24, 24, 24, 24])
            for i, pt in enumerate(parts):
                pct = pt.get('pct', 0)
                h   = 'good' if pct < 75 else ('warning' if pct < 90 else 'bad')
                self._tbl_row([
                    (pt.get('mount') or '')[:30],
                    fmt_bytes(pt.get('total', 0)),
                    fmt_bytes(pt.get('used',  0)),
                    fmt_bytes(pt.get('free',  0)),
                    f'{pct}%',
                ], [64, 24, 24, 24, 24], alt=i % 2 == 1, hl=h)
        self._pdf.ln(2)

    def _draw_memory(self, data: dict):
        mem = data.get('memory', {})
        if not mem: return
        self._sec_hdr('MEMORIE RAM')
        pct   = mem.get('pct', 0)
        total = mem.get('total', 0)
        used  = mem.get('used',  0)
        avail = mem.get('available', 0)
        h = 'good' if pct < 70 else ('warning' if pct < 90 else 'bad')
        self._kv('Ocupare',
                 f'{pct}%  ({fmt_bytes(used)} din {fmt_bytes(total)})', False, h)
        self._kv('Disponibil', fmt_bytes(avail), True)
        swt = mem.get('swap_total', 0)
        swu = mem.get('swap_used',  0)
        if swt:
            self._kv('Swap / Fisier pagina',
                     f'{fmt_bytes(swu)} / {fmt_bytes(swt)}', False)
        slots = mem.get('slots', [])
        if slots:
            self._pdf.ln(3)
            self._pdf.set_font(self._fn, 'B', 8.5)
            self._pdf.set_text_color(*self._LBL)
            self._pdf.cell(0, 5, '  Module RAM instalate:',
                           new_x='LMARGIN', new_y='NEXT')
            self._tbl_hdr(['Slot', 'Capacitate', 'Tip', 'Viteza (MHz)', 'Producator'],
                          [28, 26, 20, 26, 55])
            for i, sl in enumerate(slots):
                self._tbl_row([
                    (sl.get('slot') or 'N/A')[:12],
                    fmt_bytes(sl.get('cap', 0)) if sl.get('cap') else '—',
                    sl.get('type', '—'),
                    str(sl.get('speed', '—')),
                    (sl.get('mfr') or '—')[:25],
                ], [28, 26, 20, 26, 55], alt=i % 2 == 1)
        self._pdf.ln(2)

    def _draw_gpu(self, data: dict):
        gpu_d = data.get('gpu', {})
        gpus  = gpu_d.get('gpus', [])
        nvs   = gpu_d.get('nvidia', [])
        lhm   = gpu_d.get('lhm_temps', {})
        if not gpus: return
        self._sec_hdr('PLACA VIDEO (GPU)')
        alt = False
        for i, g in enumerate(gpus):
            name = g.get('name', 'GPU')
            self._pdf.ln(1)
            self._pdf.set_font(self._fn, 'B', 9)
            self._pdf.set_text_color(*self._C_INF)
            self._pdf.cell(0, 5.5, f'  {name}',
                           new_x='LMARGIN', new_y='NEXT')
            vram_b = g.get('vram', 0)
            if vram_b:
                self._kv('VRAM', fmt_bytes(vram_b), alt); alt = not alt
            self._kv('Driver', g.get('driver', '—'), alt); alt = not alt
            self._kv('Data driver', g.get('dd', '—'), alt); alt = not alt
            res = g.get('res', '')
            hz  = g.get('hz', 0)
            if res and res != '0x0':
                self._kv('Rezolutie', f'{res} @ {hz}Hz' if hz else res,
                         alt); alt = not alt
            # NVIDIA live data
            if i < len(nvs):
                nv = nvs[i]
                t = nv.get('temp', '')
                if t and t != 'N/A':
                    th = 'good' if int(t) < 75 else ('warning' if int(t) < 90 else 'bad')
                    self._kv('Temperatura', f'{t}°C', alt, th); alt = not alt
                gu = nv.get('gpu_u', '')
                if gu and gu != 'N/A':
                    self._kv('Utilizare GPU', f'{gu}%', alt); alt = not alt
                pw = nv.get('power', '')
                pl = nv.get('plimit', '')
                if pw and pw != 'N/A':
                    self._kv('Consum', f'{pw} W / {pl} W limit' if pl != 'N/A' else f'{pw} W',
                             alt); alt = not alt
            elif lhm:
                for sname, sval in list(lhm.items())[:2]:
                    th = 'good' if sval < 75 else ('warning' if sval < 90 else 'bad')
                    self._kv(f'Temperatura ({sname})', f'{sval}°C', alt, th)
                    alt = not alt
        self._pdf.ln(2)

    def _draw_network(self, data: dict):
        net = data.get('network', {})
        self._sec_hdr('RETEA')
        alt     = False
        online  = net.get('internet')
        lat     = net.get('latency_ms', -1)
        if online is not None:
            h   = 'good' if online else 'bad'
            msg = 'Online' if online else 'Offline'
            if online and lat and lat > 0: msg += f'  (latenta: {lat} ms)'
            self._kv('Conexiune internet', msg, alt, h); alt = not alt
        gateways = net.get('gateways', [])
        for gw in gateways[:2]:
            self._kv(f"Gateway ({gw.get('iface','')})",
                     gw.get('gateway', '—'), alt); alt = not alt
            dns_str = gw.get('dns', '')
            if dns_str:
                self._kv(f"DNS ({gw.get('iface','')})",
                         dns_str[:60], alt); alt = not alt
        # Adaptoare - merge WMI + psutil
        adapters = net.get('adapters', [])
        addrs    = net.get('addrs',    {})
        stats    = net.get('stats',    {})
        merged   = []
        for a in adapters:
            nm  = a.get('name', '')
            ips = addrs.get(nm, [])
            st  = stats.get(nm, {})
            merged.append({
                'name':  nm,
                'ip':    ips[0] if ips else '—',
                'speed': fmt_speed(a.get('speed', 0) / 1000) if a.get('speed') else '—',
                'up':    st.get('up', True),
            })
        # Adauga adaptoare din psutil care nu sunt in WMI
        wmi_names = {a.get('name','').lower() for a in adapters}
        for iface, ips in addrs.items():
            if iface.lower() not in wmi_names:
                st = stats.get(iface, {})
                if st.get('up') and ips:
                    merged.append({'name': iface, 'ip': ips[0],
                                   'speed': '—', 'up': True})
        if merged:
            self._pdf.ln(3)
            self._pdf.set_font(self._fn, 'B', 8.5)
            self._pdf.set_text_color(*self._LBL)
            self._pdf.cell(0, 5, '  Adaptoare retea active:',
                           new_x='LMARGIN', new_y='NEXT')
            self._tbl_hdr(['Adaptor', 'Adresa IPv4', 'Viteza', 'Stare'],
                          [78, 46, 28, 18])
            for i, a in enumerate(merged[:12]):
                h = 'good' if a.get('up') else 'unknown'
                self._tbl_row([
                    (a.get('name') or '')[:38],
                    a.get('ip', '—')[:20],
                    a.get('speed', '—'),
                    'Activ' if a.get('up') else 'Inactiv',
                ], [78, 46, 28, 18], alt=i % 2 == 1, hl=h)
        self._pdf.ln(2)

    def _draw_system(self, data: dict):
        sys_d = data.get('system', {})
        if not sys_d: return
        self._sec_hdr('SISTEM')
        alt = False
        cpu  = sys_d.get('cpu', {})
        freq = sys_d.get('freq', {})
        if cpu.get('name'):
            self._kv('Procesor', cpu['name'], alt); alt = not alt
        pc = cpu.get('pc') or cpu.get('cores')
        lc = cpu.get('lc') or cpu.get('threads')
        if pc:
            self._kv('Nuclee fizice / logice', f'{pc} / {lc}', alt); alt = not alt
        fmax = freq.get('max')
        if fmax:
            self._kv('Frecventa maxima', f'{fmax:.0f} MHz', alt); alt = not alt
        if cpu.get('socket'):
            self._kv('Socket', cpu['socket'], alt); alt = not alt
        temps = sys_d.get('temps', [])
        if temps:
            avg = round(sum(temps) / len(temps), 1)
            h   = 'good' if avg < 70 else ('warning' if avg < 85 else 'bad')
            self._kv('Temperatura medie CPU', f'{avg}°C', alt, h); alt = not alt
        mb = sys_d.get('mb', {})
        if mb.get('mfr') or mb.get('model'):
            self._kv('Placa de baza',
                     f"{mb.get('mfr','')} {mb.get('model','')}".strip(),
                     alt); alt = not alt
            if mb.get('ver') and mb['ver'] != 'N/A':
                self._kv('  Versiune MB', mb['ver'], alt); alt = not alt
        bios = sys_d.get('bios', {})
        if bios.get('ver'):
            self._kv('BIOS versiune', bios['ver'], alt); alt = not alt
        if bios.get('date') and bios['date'] != 'N/A':
            self._kv('BIOS data', bios['date'], alt); alt = not alt
        os_d = sys_d.get('os', {})
        if os_d.get('name'):
            self._kv('Sistem de operare', os_d['name'], alt); alt = not alt
        if os_d.get('build'):
            self._kv('Build Windows', os_d['build'], alt); alt = not alt
        if os_d.get('arch'):
            self._kv('Arhitectura', os_d['arch'], alt); alt = not alt
        if os_d.get('install'):
            self._kv('Data instalare', os_d['install'], alt); alt = not alt
        up = sys_d.get('uptime', '')
        bt = sys_d.get('boot', '')
        if up:  self._kv('Uptime',          str(up), alt); alt = not alt
        if bt:  self._kv('Ultima pornire',  str(bt), alt); alt = not alt
        self._pdf.ln(2)

    def _draw_psu(self, data: dict):
        psu = data.get('psu', {})
        if not psu: return
        self._sec_hdr('ALIMENTARE / SURSA')
        alt = False
        if psu.get('type') == 'laptop':
            pct    = psu.get('pct', 0)
            plugg  = psu.get('plugged', False)
            h      = 'good' if pct > 50 else ('warning' if pct > 20 else 'bad')
            status = 'La incarcare' if plugg else 'Pe baterie'
            self._kv('Nivel baterie', f'{pct}%  ({status})', alt, h); alt = not alt
            hp = psu.get('health_pct')
            if hp is not None:
                hh = 'good' if hp >= 80 else ('warning' if hp >= 60 else 'bad')
                dc = psu.get('design_cap', 0)
                fc = psu.get('full_cap',   0)
                cap_str = f'{fc} mWh / {dc} mWh design' if dc and fc else ''
                self._kv('Sanatate baterie',
                         f'{hp}%  ({cap_str})' if cap_str else f'{hp}%',
                         alt, hh); alt = not alt
            secs = psu.get('secs', 0)
            if secs and secs > 0 and not plugg:
                h2, m2 = divmod(int(secs), 3600)
                self._kv('Autonomie estimata',
                         f'{h2}h {m2//60}min', alt); alt = not alt
        for r in psu.get('rapl', []):
            self._kv(f"Consum RAPL ({r.get('name','')})",
                     f"{r.get('watts',0):.1f} W", alt); alt = not alt
        for vc in psu.get('vcores', []):
            self._kv(f"VCore CPU",
                     f"{vc.get('voltage',0):.2f} V", alt); alt = not alt
        pp = psu.get('power_plan', '')
        if pp and pp != 'Necunoscut':
            self._kv('Plan de alimentare Windows', pp, alt); alt = not alt
        for g in data.get('gpu', {}).get('nvidia', []):
            pw = g.get('power', '')
            if pw and pw not in ('N/A', ''):
                nm = g.get('name', 'GPU')[:20]
                self._kv(f'Consum GPU ({nm})', f'{pw} W', alt); alt = not alt
        self._pdf.ln(2)

    # ── Export principal ──────────────────────────────
    def export(self, data: dict, path: str):
        try:
            from fpdf import FPDF
        except ImportError:
            raise RuntimeError(
                'fpdf2 nu este instalat. Ruleaza: pip install fpdf2')

        _ts  = data.get('ts', '')
        _ver = VERSION

        # Fontul implicit; Arial TTF va fi activat dupa crearea pdf-ului
        _af  = r'C:\Windows\Fonts\arial.ttf'
        _afb = r'C:\Windows\Fonts\arialbd.ttf'
        _afi = r'C:\Windows\Fonts\ariali.ttf'
        _fn  = 'Arial' if os.path.exists(_af) else 'Helvetica'
        self._fn = _fn

        class _PDF(FPDF):
            def footer(slf):
                slf.set_y(-13)
                slf.set_draw_color(210, 215, 220)
                slf.set_line_width(0.25)
                slf.line(15, slf.get_y(), 195, slf.get_y())
                slf.set_font(_fn, 'I', 7.5)
                slf.set_text_color(155, 155, 155)
                slf.cell(
                    0, 6,
                    f'PC Health Monitor v{_ver}  |  {_ts}  |  Pagina {slf.page_no()}',
                    align='C')

        pdf = _PDF('P', 'mm', 'A4')
        pdf.set_auto_page_break(True, margin=20)
        pdf.set_left_margin(15)
        pdf.set_right_margin(15)
        pdf.set_top_margin(15)
        pdf.set_title('PC Health Report')
        pdf.set_author(f'PC Health Monitor v{VERSION}')

        # Adauga fontul Unicode la instanta actuala
        if _fn == 'Arial':
            try:
                pdf.add_font('Arial', fname=_af)
                if os.path.exists(_afb):
                    pdf.add_font('Arial', style='B', fname=_afb)
                if os.path.exists(_afi):
                    pdf.add_font('Arial', style='I', fname=_afi)
            except Exception:
                self._fn = 'Helvetica'

        self._pdf = pdf
        self._W   = 180

        pdf.add_page()
        self._draw_page_header(data)
        self._draw_score(data)
        self._draw_overview(data)
        self._draw_security(data)
        self._draw_storage(data)
        self._draw_memory(data)
        self._draw_gpu(data)
        self._draw_network(data)
        self._draw_system(data)
        self._draw_psu(data)

        pdf.output(path)

# ═══════════════════════════════════════════════════════
#  App
# ═══════════════════════════════════════════════════════
class App(ctk.CTk):
    def __init__(self):
        super().__init__()
        ctk.set_appearance_mode("dark")
        ctk.set_default_color_theme("blue")
        self.title(f"{TITLE} v{VERSION}")
        self.geometry("1120x800")
        self.minsize(900, 660)

        self._store   = DataStore()
        self._sampler = Sampler(self._store)
        self._col     = HardwareCollector()
        self._exp     = HTMLExporter()
        self._pdf_exp = PDFExporter()
        self._data: dict = {}
        self._busy    = False
        self._ar_id   = None
        self._tick_id = None
        self._proc_tick = 0

        # Widget refs updated in tick
        self._all_graphs: list[MiniGraph] = []
        self._core_bars:  list[tuple]     = []  # (bar, label_pct)
        self._proc_cpu_rows = []
        self._proc_ram_rows = []
        self._sb_labels: dict = {}

        self._build_ui()
        self._sampler.start()
        self._tick()
        self.after(300, self._do_refresh)
        self.protocol("WM_DELETE_WINDOW", self._on_close)

    # ─── UI Construction ────────────────────────────────
    def _build_ui(self):
        # Header
        hdr = ctk.CTkFrame(self, height=58, corner_radius=0, fg_color=('#111827', '#080c14'))
        hdr.pack(fill='x')
        hdr.pack_propagate(False)

        ctk.CTkLabel(hdr, text=f"  {TITLE}",
                     font=ctk.CTkFont(size=21, weight='bold')).pack(side='left', padx=20)

        self._ts_lbl = ctk.CTkLabel(hdr, text="", text_color='gray', font=ctk.CTkFont(size=11))
        self._ts_lbl.pack(side='right', padx=14)

        if TRAY_OK:
            ctk.CTkButton(hdr, text="Tray", width=60, height=32,
                          fg_color='#1f2937', hover_color='#374151',
                          command=self._to_tray).pack(side='right', padx=4, pady=13)

        ctk.CTkButton(hdr, text="Export HTML", width=110, height=32,
                      command=self._export_html).pack(side='right', padx=4, pady=13)

        self._cloud_lbl = ctk.CTkLabel(hdr, text='', font=('Segoe UI', 10),
                                       text_color='#64748b')
        self._cloud_lbl.pack(side='right', padx=2, pady=13)

        ctk.CTkButton(hdr, text="☁ Cloud", width=88, height=32,
                      fg_color='#1e3a5f', hover_color='#2d5a8e',
                      command=self._show_cloud_dialog).pack(side='right', padx=4, pady=13)

        ctk.CTkButton(hdr, text="Export PDF", width=105, height=32,
                      fg_color='#6d28d9', hover_color='#5b21b6',
                      command=self._export_pdf).pack(side='right', padx=4, pady=13)

        ctk.CTkButton(hdr, text="Export JSON", width=105, height=32,
                      fg_color='#065f46', hover_color='#047857',
                      command=self._export_json).pack(side='right', padx=4, pady=13)

        self._refresh_btn = ctk.CTkButton(hdr, text="Actualizeaza", width=130, height=32,
                                          command=self._do_refresh)
        self._refresh_btn.pack(side='right', padx=4, pady=13)

        ar_opts = ['Manual', '5s', '10s', '30s', '60s']
        self._ar_var = tk.StringVar(value='Manual')
        ctk.CTkComboBox(hdr, values=ar_opts, variable=self._ar_var, width=90, height=32,
                        command=lambda _: self._schedule_ar()).pack(side='right', padx=6, pady=13)
        ctk.CTkLabel(hdr, text="Auto:", font=ctk.CTkFont(size=12),
                     text_color='gray').pack(side='right', padx=2)

        # Tabs
        self._tabs = ctk.CTkTabview(self, corner_radius=8)
        self._tabs.pack(fill='both', expand=True, padx=10, pady=(6, 4))

        TAB_NAMES = ['Sumar', 'Procesor', 'Stocare', 'Memorie', 'GPU',
                     'Retea', 'Procese', 'Sistem', 'Securitate', 'Sursa']
        for t in TAB_NAMES:
            self._tabs.add(t)

        self._setup_summary_tab(self._tabs.tab('Sumar'))
        self._setup_cpu_tab(self._tabs.tab('Procesor'))
        self._setup_storage_tab(self._tabs.tab('Stocare'))
        self._setup_memory_tab(self._tabs.tab('Memorie'))
        self._setup_gpu_tab(self._tabs.tab('GPU'))
        self._setup_network_tab(self._tabs.tab('Retea'))
        self._setup_processes_tab(self._tabs.tab('Procese'))
        self._setup_system_tab(self._tabs.tab('Sistem'))
        self._setup_security_tab(self._tabs.tab('Securitate'))
        self._setup_psu_tab(self._tabs.tab('Sursa'))

        # Status bar
        sb = ctk.CTkFrame(self, height=28, corner_radius=0, fg_color=('#1f2937', '#0d1117'))
        sb.pack(fill='x', side='bottom')
        sb.pack_propagate(False)
        for key, txt in [('cpu','CPU: --'), ('ram','RAM: --'),
                          ('net','Net ↓/↑: --'), ('disk','Disk R/W: --')]:
            lbl = ctk.CTkLabel(sb, text=txt, font=ctk.CTkFont(size=11), text_color='gray')
            lbl.pack(side='left', padx=18)
            self._sb_labels[key] = lbl

    # ─── Tab setup (called once) ─────────────────────────
    def _graph_frame(self, parent, title, height=110):
        outer = ctk.CTkFrame(parent, fg_color=('#1e293b', '#0d1220'), corner_radius=8)
        outer.pack(fill='x', padx=8, pady=(6, 2))
        ctk.CTkLabel(outer, text=f"  {title}", font=ctk.CTkFont(size=12, weight='bold'),
                     text_color='gray', anchor='w').pack(anchor='w', padx=8, pady=(4, 0))
        inner = ctk.CTkFrame(outer, fg_color=GRAPH_BG, corner_radius=6, height=height)
        inner.pack(fill='x', padx=8, pady=(2, 8))
        inner.pack_propagate(False)
        return inner

    def _add_graph(self, parent, data, color, unit='%', scale=100.0):
        g = MiniGraph(parent, data, color=color, unit=unit, scale=scale)
        g.pack(fill='both', expand=True, padx=4, pady=4)
        self._all_graphs.append(g)
        return g

    def _scroll(self, parent):
        sf = ctk.CTkScrollableFrame(parent, corner_radius=0, fg_color='transparent')
        sf.pack(fill='both', expand=True, padx=2)
        return sf

    def _side_graphs(self, parent, configs):
        top = ctk.CTkFrame(parent, fg_color='transparent')
        top.pack(fill='x', padx=6)
        for title, data, color, unit, scale in configs:
            outer = ctk.CTkFrame(top, fg_color=('#1e293b','#0d1220'), corner_radius=8)
            outer.pack(side='left', fill='x', expand=True, padx=3, pady=6)
            ctk.CTkLabel(outer, text=f"  {title}", font=ctk.CTkFont(size=12, weight='bold'),
                         text_color='gray', anchor='w').pack(anchor='w', padx=8, pady=(4,0))
            inner = ctk.CTkFrame(outer, fg_color=GRAPH_BG, corner_radius=6, height=100)
            inner.pack(fill='x', padx=8, pady=(2,8))
            inner.pack_propagate(False)
            self._add_graph(inner, data, color, unit=unit, scale=scale)

    def _setup_summary_tab(self, parent):
        self._side_graphs(parent, [
            ("CPU Live", self._store.cpu,  '#4f8ef7', '%', 100.0),
            ("RAM Live", self._store.ram,  '#a855f7', '%', 100.0),
        ])
        self._sum_scroll = self._scroll(parent)

    def _setup_cpu_tab(self, parent):
        f = self._graph_frame(parent, "Utilizare CPU total", height=120)
        self._add_graph(f, self._store.cpu, '#4f8ef7')
        self._cpu_scroll = self._scroll(parent)

    def _setup_storage_tab(self, parent):
        self._side_graphs(parent, [
            ("Citire disc (MB/s)",  self._store.disk_r, '#22c55e', 'MB/s', 500.0),
            ("Scriere disc (MB/s)", self._store.disk_w, '#f59e0b', 'MB/s', 500.0),
        ])
        self._stor_scroll = self._scroll(parent)

    def _setup_memory_tab(self, parent):
        f = self._graph_frame(parent, "Utilizare RAM", height=110)
        self._add_graph(f, self._store.ram, '#a855f7')
        self._mem_scroll = self._scroll(parent)

    def _setup_gpu_tab(self, parent):
        self._side_graphs(parent, [
            ("Temperatura GPU (°C)", self._store.gpu_t, '#ef4444', '°C',  100.0),
            ("Utilizare GPU (%)",    self._store.gpu_u, '#f97316', '%',   100.0),
        ])
        self._gpu_scroll = self._scroll(parent)

    def _setup_network_tab(self, parent):
        self._side_graphs(parent, [
            ("Download (KB/s)", self._store.net_rx, '#06b6d4', 'KB/s', 10240.0),
            ("Upload (KB/s)",   self._store.net_tx, '#8b5cf6', 'KB/s', 10240.0),
        ])
        self._net_scroll = self._scroll(parent)

    def _setup_processes_tab(self, parent):
        # Header row
        hdr = ctk.CTkFrame(parent, fg_color=('#1e293b','#0a0e1a'), height=32, corner_radius=0)
        hdr.pack(fill='x', padx=8, pady=(6,0))
        hdr.pack_propagate(False)
        for txt, w, side in [('Proces (CPU%)', 220,'left'), ('CPU%',60,'left'),
                              ('RAM',90,'left'), ('   |   Proces (RAM)',200,'left'),
                              ('RAM',90,'left'), ('CPU%',60,'left')]:
            ctk.CTkLabel(hdr, text=txt, width=w, anchor='w',
                         font=ctk.CTkFont(size=11, weight='bold'),
                         text_color='#8b949e').pack(side=side, padx=4)

        self._proc_frame = ctk.CTkScrollableFrame(parent, fg_color='transparent')
        self._proc_frame.pack(fill='both', expand=True, padx=8, pady=4)

        for _ in range(10):
            row = self._make_proc_row(self._proc_frame)
            self._proc_cpu_rows.append(row)

    def _make_proc_row(self, parent):
        f = ctk.CTkFrame(parent, fg_color='transparent', height=26)
        f.pack(fill='x', pady=1)
        f.pack_propagate(False)
        labels = {}
        for key, w, side in [('name', 220, 'left'), ('cpu', 60, 'left'),
                              ('ram', 90, 'left')]:
            lbl = ctk.CTkLabel(f, text='', width=w, anchor='w',
                               font=ctk.CTkFont(size=11, family='Consolas'))
            lbl.pack(side=side, padx=4)
            labels[key] = lbl
        return labels

    def _setup_system_tab(self, parent):
        self._sys_scroll = self._scroll(parent)

    def _setup_security_tab(self, parent):
        self._sec_scroll = self._scroll(parent)

    def _setup_psu_tab(self, parent):
        self._psu_scroll = self._scroll(parent)

    # ─── Tick loop (1 Hz) ───────────────────────────────
    def _tick(self):
        for g in self._all_graphs:
            try: g.refresh()
            except Exception: pass

        # Per-core bars
        with self._store._lock:
            cores_snap = [list(d)[-1] if d else 0 for d in self._store.cores]
        for i, (bar, lbl) in enumerate(self._core_bars):
            if i < len(cores_snap):
                v = cores_snap[i]
                try:
                    bar.set(v / 100)
                    col = HC['good'] if v < 75 else (HC['warning'] if v < 90 else HC['bad'])
                    bar.configure(progress_color=col)
                    lbl.configure(text=f'{v:.0f}%')
                except Exception: pass

        # Status bar
        cpu_v = list(self._store.cpu)[-1] if self._store.cpu else 0
        ram_v = list(self._store.ram)[-1] if self._store.ram else 0
        rx_v  = list(self._store.net_rx)[-1] if self._store.net_rx else 0
        tx_v  = list(self._store.net_tx)[-1] if self._store.net_tx else 0
        dr_v  = list(self._store.disk_r)[-1] if self._store.disk_r else 0
        dw_v  = list(self._store.disk_w)[-1] if self._store.disk_w else 0

        def sb_col(v, key): return self._hc(health_of(v, key))
        try:
            self._sb_labels['cpu'].configure(text=f'CPU: {cpu_v:.0f}%',
                                             text_color=sb_col(cpu_v,'cpu'))
            self._sb_labels['ram'].configure(text=f'RAM: {ram_v:.0f}%',
                                             text_color=sb_col(ram_v,'ram'))
            self._sb_labels['net'].configure(text=f'Net ↓{fmt_speed(rx_v)} ↑{fmt_speed(tx_v)}',
                                             text_color='#64748b')
            self._sb_labels['disk'].configure(
                text=f'Disc R:{dr_v:.1f}MB/s W:{dw_v:.1f}MB/s', text_color='#64748b')
        except Exception: pass

        # Processes (every 3s)
        self._proc_tick += 1
        if self._proc_tick % 3 == 0:
            threading.Thread(target=self._update_processes, daemon=True).start()

        self._tick_id = self.after(1000, self._tick)

    def _update_processes(self):
        try:
            procs = []
            for p in psutil.process_iter(['pid','name','cpu_percent','memory_info']):
                try:
                    mi = p.info['memory_info']
                    procs.append({'name': p.info['name'] or '?',
                                  'pid':  p.info['pid'],
                                  'cpu':  p.info['cpu_percent'] or 0,
                                  'ram':  mi.rss if mi else 0})
                except Exception: pass
            top_cpu = sorted(procs, key=lambda x: x['cpu'], reverse=True)[:10]
            top_ram = sorted(procs, key=lambda x: x['ram'], reverse=True)[:10]
            self.after(0, lambda: self._draw_processes(top_cpu, top_ram))
        except Exception: pass

    def _draw_processes(self, top_cpu, top_ram):
        for f in self._proc_frame.winfo_children():
            f.destroy()
        self._proc_cpu_rows.clear()
        self._proc_ram_rows.clear()

        n = max(len(top_cpu), len(top_ram))
        for i in range(n):
            row = ctk.CTkFrame(self._proc_frame, fg_color=('#1a2035','#0d1117') if i%2==0 else 'transparent',
                               corner_radius=4, height=26)
            row.pack(fill='x', pady=1)
            row.pack_propagate(False)

            # CPU side
            cpu_proc = top_cpu[i] if i < len(top_cpu) else None
            if cpu_proc:
                nm = cpu_proc['name'][:24]
                cv = cpu_proc['cpu']
                ccol = HC['good'] if cv < 30 else (HC['warning'] if cv < 70 else HC['bad'])
                ctk.CTkLabel(row, text=nm, width=210, anchor='w',
                             font=ctk.CTkFont(size=11, family='Consolas')).pack(side='left', padx=(6,2))
                ctk.CTkLabel(row, text=f'{cv:.1f}%', width=58, anchor='e',
                             text_color=ccol, font=ctk.CTkFont(size=11, family='Consolas')).pack(side='left', padx=2)
                ctk.CTkLabel(row, text=fmt_bytes(cpu_proc['ram']), width=85, anchor='e',
                             text_color='#888', font=ctk.CTkFont(size=11, family='Consolas')).pack(side='left', padx=2)
            else:
                ctk.CTkFrame(row, width=355, fg_color='transparent').pack(side='left')

            # Separator
            ctk.CTkLabel(row, text=' │ ', text_color='#1e293b', font=ctk.CTkFont(size=11)).pack(side='left')

            # RAM side
            ram_proc = top_ram[i] if i < len(top_ram) else None
            if ram_proc:
                nm = ram_proc['name'][:24]
                rv = ram_proc['ram']
                cv2 = ram_proc['cpu']
                ctk.CTkLabel(row, text=nm, width=210, anchor='w',
                             font=ctk.CTkFont(size=11, family='Consolas')).pack(side='left', padx=(6,2))
                ctk.CTkLabel(row, text=fmt_bytes(rv), width=85, anchor='e',
                             text_color='#a855f7', font=ctk.CTkFont(size=11, family='Consolas')).pack(side='left', padx=2)
                ctk.CTkLabel(row, text=f'{cv2:.1f}%', width=55, anchor='e',
                             text_color='#888', font=ctk.CTkFont(size=11, family='Consolas')).pack(side='left', padx=2)

    # ─── Full refresh ────────────────────────────────────
    def _do_refresh(self):
        if self._busy: return
        self._busy = True
        self._refresh_btn.configure(state='disabled', text='Se incarca...')
        self._ts_lbl.configure(text='Se citesc datele...')
        threading.Thread(target=self._bg, daemon=True).start()

    def _bg(self):
        try: self._data = self._col.collect()
        except Exception as e: self._data = {'error': str(e), 'ts': datetime.now().strftime('%d.%m.%Y %H:%M:%S')}
        self.after(0, self._render)

    def _render(self):
        d = self._data
        threading.Thread(target=_save_history, args=(d,), daemon=True).start()
        threading.Thread(target=self._cloud_sync, args=(d,), daemon=True).start()
        self._render_summary(d)
        self._render_cpu(d.get('system', {}))
        self._render_storage(d.get('storage', {}))
        self._render_memory(d.get('memory', {}))
        self._render_gpu(d.get('gpu', {}))
        self._render_network(d.get('network', {}))
        self._render_system(d.get('system', {}), d.get('fans', []), d.get('perf_extras', {}), d.get('security', {}))
        self._render_security(d.get('security', {}), d.get('network', {}))
        self._render_psu(d.get('psu', {}))
        self._ts_lbl.configure(text=f"Actualizat: {d.get('ts','')}")
        self._refresh_btn.configure(state='normal', text='Actualizeaza')
        self._busy = False
        self._schedule_ar()

    # ─── Widget helpers ──────────────────────────────────
    @staticmethod
    def _hc(h): return HC.get(h, HC['unknown'])

    def _clr(self, sf):
        for w in sf.winfo_children(): w.destroy()

    def _card(self, parent, title, border=None):
        outer = ctk.CTkFrame(parent, border_width=1,
                             border_color=border or '#1e293b', corner_radius=10)
        outer.pack(fill='x', padx=8, pady=5)
        ctk.CTkLabel(outer, text=f"  {title}",
                     font=ctk.CTkFont(size=14, weight='bold'), anchor='w').pack(fill='x', padx=12, pady=(10,3))
        ctk.CTkFrame(outer, height=1, fg_color='#1e293b').pack(fill='x', padx=10, pady=(0,4))
        return outer

    def _row(self, p, label, val, col=None):
        r = ctk.CTkFrame(p, fg_color='transparent')
        r.pack(fill='x', padx=14, pady=2)
        ctk.CTkLabel(r, text=label, width=220, anchor='w',
                     text_color='#64748b', font=ctk.CTkFont(size=12)).pack(side='left')
        ctk.CTkLabel(r, text=str(val), anchor='w',
                     text_color=col or '#cbd5e1', font=ctk.CTkFont(size=12)).pack(side='left', padx=4)

    def _badge(self, parent, health, text):
        col = self._hc(health)
        fg  = '#000' if health in ('good', 'warning', 'unknown') else '#fff'
        ctk.CTkLabel(parent, text=f"  {text}  ", fg_color=col, corner_radius=7,
                     text_color=fg, font=ctk.CTkFont(size=11, weight='bold')).pack(side='left', padx=3)

    def _pbar(self, parent, pct, label='', h=13):
        r = ctk.CTkFrame(parent, fg_color='transparent')
        r.pack(fill='x', padx=14, pady=3)
        if label:
            ctk.CTkLabel(r, text=label, width=260, anchor='w',
                         text_color='#64748b', font=ctk.CTkFont(size=12)).pack(side='left')
        col = HC['good'] if pct <= 79 else (HC['warning'] if pct <= 91 else HC['bad'])
        bar = ctk.CTkProgressBar(r, height=h)
        bar.set(max(0.0, min(1.0, pct / 100)))
        bar.configure(progress_color=col)
        bar.pack(side='left', fill='x', expand=True, padx=(0,8))
        ctk.CTkLabel(r, text=f'{pct:.1f}%', width=52, font=ctk.CTkFont(size=12)).pack(side='left')
        return bar

    def _gap(self, p, h=6):
        ctk.CTkFrame(p, height=h, fg_color='transparent').pack()

    # ─── Summary ─────────────────────────────────────────
    def _health_items(self, d):
        items = []
        # Storage
        disks = d.get('storage', {}).get('disks', [])
        parts = d.get('storage', {}).get('parts', [])
        if not disks:
            items.append(('HDD/SSD', 'Stocare', 'unknown', 'Fara date'))
        else:
            bad  = [x for x in disks if x['health']=='bad']
            warn = [x for x in disks if x['health']=='warning']
            full = [p for p in parts if p['pct']>90]
            if bad:   items.append(('HDD/SSD','Stocare','bad',   f'{len(bad)} disc(uri) cu erori SMART'))
            elif warn: items.append(('HDD/SSD','Stocare','warning',f'{len(warn)} avertisment(e)'))
            elif full: items.append(('HDD/SSD','Stocare','warning',f'{len(full)} partitie(i) aproape plina'))
            else:
                temps = [x['temp'] for x in disks if x.get('temp')]
                if temps:
                    items.append(('HDD/SSD', 'Stocare', 'good',
                                  f'{len(disks)} disc(uri) OK — {max(temps)}°C max'))
                else:
                    items.append(('HDD/SSD', 'Stocare', 'good', f'{len(disks)} disc(uri) OK'))
        # RAM
        pct = d.get('memory',{}).get('pct', 0)
        items.append(('RAM','Memorie',health_of(pct,'ram'),
                      f'Critica: {pct:.0f}%' if pct>93 else
                      f'Ridicata: {pct:.0f}%' if pct>80 else f'{pct:.0f}% utilizata'))
        # GPU
        gpus = d.get('gpu',{}).get('gpus',[])
        nv   = d.get('gpu',{}).get('nvidia',[])
        if not gpus: items.append(('GPU','Placa video','unknown','Nicio placa detectata'))
        else:
            bg = [g for g in gpus if g['health']=='bad']
            if bg: items.append(('GPU','Placa video','bad',f'{len(bg)} GPU cu probleme'))
            elif nv:
                try:
                    temp = float(nv[0]['temp'])
                    h = health_of(temp,'gpu_temp')
                    items.append(('GPU','Placa video',h,f'{len(gpus)} GPU — {temp:.0f}°C'))
                except: items.append(('GPU','Placa video','good',f'{len(gpus)} GPU OK'))
            else: items.append(('GPU','Placa video','good',f'{len(gpus)} GPU OK'))
        # CPU/System
        cpu_u = d.get('system',{}).get('cpu',{}).get('usage',0)
        temps = d.get('system',{}).get('temps',[])
        if temps:
            avg = sum(temps)/len(temps)
            h = health_of(avg,'cpu_temp')
            items.append(('CPU','Sistem',h,f'CPU {cpu_u:.0f}% • {avg:.0f}°C'))
        else:
            items.append(('CPU','Sistem',health_of(cpu_u,'cpu'),f'CPU {cpu_u:.0f}%'))
        # PSU / Battery
        psu = d.get('psu', {})
        if psu.get('type') == 'laptop':
            bp   = psu.get('pct', 0)
            plug = psu.get('plugged', False)
            hp   = psu.get('health_pct')
            if hp is not None and hp < 60:
                items.append(('PSU', 'Sursa', 'bad',     f'Sanatate baterie critica: {hp:.0f}%'))
            elif hp is not None and hp < 80:
                items.append(('PSU', 'Sursa', 'warning', f'Sanatate baterie: {hp:.0f}%'))
            elif not plug and bp < 15:
                items.append(('PSU', 'Sursa', 'bad',     f'Baterie critica: {bp:.0f}%'))
            elif not plug and bp < 35:
                items.append(('PSU', 'Sursa', 'warning', f'Baterie: {bp:.0f}%'))
            elif plug:
                txt = 'Conectat la curent'
                if hp: txt += f' — sanatate {hp:.0f}%'
                items.append(('PSU', 'Sursa', 'good', txt))
            else:
                txt = f'Baterie: {bp:.0f}%'
                if hp: txt += f' — sanatate {hp:.0f}%'
                items.append(('PSU', 'Sursa', 'good', txt))
        else:
            psu  = d.get('psu', {})
            rapl = psu.get('rapl', [])
            if rapl:
                total_w = sum(x['watts'] for x in rapl)
                pwr_h   = 'good' if total_w < 200 else ('warning' if total_w < 400 else 'bad')
                items.append(('PSU', 'Sursa', pwr_h, f'Consum masurat: ~{total_w:.0f} W'))
            elif psu.get('vcores'):
                vc = psu['vcores'][0]
                items.append(('PSU', 'Sursa', 'good', f"VCore: {vc['voltage']} V"))
            else:
                items.append(('PSU', 'Sursa', 'unknown', 'RAPL indisponibil pe acest procesor'))
        return items

    def _render_summary(self, d):
        self._clr(self._sum_scroll)
        f = self._sum_scroll
        ctk.CTkLabel(f, text="Starea de sanatate a calculatorului",
                     font=ctk.CTkFont(size=19, weight='bold')).pack(pady=(14,3))
        ctk.CTkLabel(f, text=f"Ultima actualizare: {d.get('ts','N/A')}",
                     text_color='gray', font=ctk.CTkFont(size=11)).pack(pady=(0,6))

        # Health score
        score, grade, cats = _calc_health_score(d)
        sc_col = self._hc(grade)
        sc_row = ctk.CTkFrame(f, fg_color='transparent')
        sc_row.pack(pady=(0, 4))
        ctk.CTkLabel(sc_row, text=str(score),
                     font=ctk.CTkFont(size=56, weight='bold'),
                     text_color=sc_col).pack(side='left', padx=(0, 4))
        sc_info = ctk.CTkFrame(sc_row, fg_color='transparent')
        sc_info.pack(side='left', padx=4)
        ctk.CTkLabel(sc_info, text="/ 100", font=ctk.CTkFont(size=18),
                     text_color='gray').pack(anchor='w')
        grade_map = {'good': 'BINE', 'warning': 'ATENTIE', 'bad': 'PROBLEMA'}
        ctk.CTkLabel(sc_info, text=grade_map.get(grade, 'N/A'),
                     font=ctk.CTkFont(size=15, weight='bold'),
                     text_color=sc_col).pack(anchor='w')
        # Category chips
        if cats:
            cat_row = ctk.CTkFrame(f, fg_color='transparent')
            cat_row.pack(fill='x', padx=20, pady=(0, 10))
            for cname, cgot, cmax in cats:
                cpct = round(cgot / cmax * 100) if cmax else 0
                ch = 'good' if cpct >= 75 else ('warning' if cpct >= 50 else 'bad')
                chip = ctk.CTkFrame(cat_row, fg_color=('#1e293b','#0d1220'), corner_radius=6)
                chip.pack(side='left', padx=3, pady=2)
                ctk.CTkLabel(chip, text=cname, font=ctk.CTkFont(size=10),
                             text_color='#64748b').pack(pady=(4,0), padx=8)
                ctk.CTkLabel(chip, text=f"{cgot}/{cmax}",
                             font=ctk.CTkFont(size=11, weight='bold'),
                             text_color=self._hc(ch)).pack(pady=(0,4), padx=8)

        items  = self._health_items(d)
        n_bad  = sum(1 for _,_,h,_ in items if h=='bad')
        n_warn = sum(1 for _,_,h,_ in items if h=='warning')
        ov_h   = 'bad' if n_bad else ('warning' if n_warn else 'good')
        ov_t   = (f'{n_bad} problema(e) critica(e)' if n_bad else
                  f'{n_warn} avertisment(e)' if n_warn else 'Calculatorul este sanatos!')
        row0 = ctk.CTkFrame(f, fg_color='transparent')
        row0.pack(pady=(0,12))
        self._badge(row0, ov_h, f'  {ov_t}  ')

        for icon, title, health, msg in items:
            col  = self._hc(health)
            c    = ctk.CTkFrame(f, border_width=2, border_color=col, corner_radius=12)
            c.pack(fill='x', padx=12, pady=3)
            inn  = ctk.CTkFrame(c, fg_color='transparent')
            inn.pack(fill='x', padx=14, pady=8)
            ctk.CTkLabel(inn, text=icon, width=75, font=ctk.CTkFont(size=11, weight='bold'),
                         text_color=col).pack(side='left')
            cc = ctk.CTkFrame(inn, fg_color='transparent')
            cc.pack(side='left', fill='x', expand=True, padx=8)
            ctk.CTkLabel(cc, text=title, font=ctk.CTkFont(size=13, weight='bold'), anchor='w').pack(anchor='w')
            ctk.CTkLabel(cc, text=msg, text_color='gray', font=ctk.CTkFont(size=11), anchor='w').pack(anchor='w')
            lmap = {'good':'OK','warning':'Atentie','bad':'Problema!','unknown':'N/A'}
            ctk.CTkLabel(inn, text=lmap.get(health,'N/A'), width=80,
                         font=ctk.CTkFont(size=12, weight='bold'), text_color=col).pack(side='right')

        # Live resource bars
        self._gap(f,8)
        sec = self._card(f, "Resurse in timp real")
        mem = d.get('memory',{})
        if mem.get('total'):
            self._pbar(sec, mem.get('pct',0),
                       f"RAM  {fmt_bytes(mem.get('used',0))} / {fmt_bytes(mem.get('total',0))}")
        self._pbar(sec, d.get('system',{}).get('cpu',{}).get('usage',0), "CPU")
        for p in d.get('storage',{}).get('parts',[]):
            self._pbar(sec, p['pct'],
                       f"Disc {p['mp']}  {fmt_bytes(p['used'])}/{fmt_bytes(p['total'])}")
        self._gap(sec, 6)

    # ─── CPU ─────────────────────────────────────────────
    def _render_cpu(self, data):
        self._clr(self._cpu_scroll)
        f = self._cpu_scroll
        cpu   = data.get('cpu', {})
        freq  = data.get('freq', {})
        temps = data.get('temps', [])

        # Info card
        card = self._card(f, "Procesor")
        self._pbar(card, cpu.get('usage', 0), "Utilizare totala")
        self._row(card, "Model", cpu.get('name', 'N/A'))
        self._row(card, "Nuclee fizice / logice",
                  f"{cpu.get('pc','?')} / {cpu.get('lc','?')}")
        if freq:
            self._row(card, "Frecventa curenta", f"{freq.get('cur',0):.0f} MHz")
            self._row(card, "Frecventa maxima",  f"{freq.get('max',0):.0f} MHz")
        if 'socket' in cpu: self._row(card, "Socket", cpu['socket'])
        if temps:
            avg = sum(temps)/len(temps)
            th  = health_of(avg,'cpu_temp')
            self._row(card, "Temperatura (ACPI)", f"{avg:.1f}°C", self._hc(th))
        self._gap(card, 6)

        # Per-core grid
        with self._store._lock:
            n_cores = len(self._store.cores)
            cores_now = [list(d)[-1] if d else 0 for d in self._store.cores]

        if n_cores:
            card2 = self._card(f, f"Per-core ({n_cores} nuclee logice)")
            grid  = ctk.CTkFrame(card2, fg_color='transparent')
            grid.pack(fill='x', padx=12, pady=8)
            cols  = 4 if n_cores <= 16 else 6
            self._core_bars.clear()
            for i in range(n_cores):
                grid.columnconfigure(i % cols, weight=1)
                cell = ctk.CTkFrame(grid, fg_color=('#1a2035','#0d1117'), corner_radius=6)
                cell.grid(row=i // cols, column=i % cols, padx=4, pady=4, sticky='ew')
                ctk.CTkLabel(cell, text=f'C{i}', font=ctk.CTkFont(size=10),
                             text_color='#475569').pack(pady=(4,0))
                lbl = ctk.CTkLabel(cell, text=f'{cores_now[i]:.0f}%',
                                   font=ctk.CTkFont(size=10, weight='bold'))
                lbl.pack()
                v = cores_now[i]
                col = HC['good'] if v<75 else (HC['warning'] if v<90 else HC['bad'])
                bar = ctk.CTkProgressBar(cell, height=8, orientation='horizontal')
                bar.set(v/100); bar.configure(progress_color=col)
                bar.pack(fill='x', padx=6, pady=(2,6))
                self._core_bars.append((bar, lbl))
            self._gap(card2, 4)

    # ─── Storage ─────────────────────────────────────────
    def _render_storage(self, data):
        self._clr(self._stor_scroll)
        f = self._stor_scroll
        for disk in data.get('disks', []):
            col  = self._hc(disk['health'])
            card = self._card(f, f"Disc: {disk['model']}", border=col)
            br   = ctk.CTkFrame(card, fg_color='transparent')
            br.pack(fill='x', padx=14, pady=(2,8))
            ctk.CTkLabel(br, text="Stare SMART: ", text_color='gray',
                         font=ctk.CTkFont(size=12)).pack(side='left')
            self._badge(br, disk['health'], disk['hmsg'])
            self._row(card, "Dimensiune",  fmt_bytes(disk['size']))
            self._row(card, "Interfata",   disk['iface'])
            self._row(card, "Tip media",   disk['media'])
            self._row(card, "Serial",      disk['serial'])
            # ── SMART detaliat ──────────────────────────
            temp = disk.get('temp')
            if temp is not None:
                is_ssd = any(k in (disk.get('media') or '').lower() for k in ('ssd', 'solid', 'nvme'))
                th = health_of(temp, 'ssd_temp' if is_ssd else 'hdd_temp')
                self._row(card, "Temperatura", f"{temp}°C", self._hc(th))
            hours = disk.get('hours')
            if hours is not None and hours > 0:
                yrs  = hours // 8760
                mths = (hours % 8760) // 720
                suf  = f"  (~{yrs}a {mths}l)" if yrs else (f"  (~{mths} luni)" if mths else "")
                self._row(card, "Ore functionare", f"{hours:,} ore{suf}")
            wear = disk.get('wear')
            if wear is not None and 0 <= wear <= 100:
                wcol = HC['good'] if wear >= 40 else (HC['warning'] if wear >= 10 else HC['bad'])
                wr   = ctk.CTkFrame(card, fg_color='transparent')
                wr.pack(fill='x', padx=14, pady=3)
                ctk.CTkLabel(wr, text="Viata ramasa (SSD)", width=260, anchor='w',
                             text_color='#64748b', font=ctk.CTkFont(size=12)).pack(side='left')
                wbar = ctk.CTkProgressBar(wr, height=13)
                wbar.set(wear / 100)
                wbar.configure(progress_color=wcol)
                wbar.pack(side='left', fill='x', expand=True, padx=(0, 8))
                ctk.CTkLabel(wr, text=f'{wear}%', width=52,
                             font=ctk.CTkFont(size=12)).pack(side='left')
            r_err = disk.get('read_err')
            w_err = disk.get('write_err')
            if r_err is not None or w_err is not None:
                total = (r_err or 0) + (w_err or 0)
                ecol  = HC['good'] if total == 0 else HC['warning']
                self._row(card, "Erori necorectate",
                          f"Citire: {r_err or 0}  Scriere: {w_err or 0}", ecol)
            self._gap(card, 4)
        parts = data.get('parts', [])
        if parts:
            card2 = self._card(f, "Utilizare volume")
            for p in parts:
                lbl = f"{p['mp']}  {fmt_bytes(p['used'])}/{fmt_bytes(p['total'])}  ({p['fs']})"
                self._pbar(card2, p['pct'], lbl)
            self._gap(card2, 6)
        note = self._card(f, "Info")
        ctk.CTkLabel(note, anchor='w', justify='left', text_color='#475569',
                     font=ctk.CTkFont(size=11),
                     text=("  Date SMART: temperatura, ore functionare, uzura SSD si erori necorectate\n"
                           "  sunt citite via Get-StorageReliabilityCounter. Graficele de I/O se actualizeaza live.")
                     ).pack(padx=14, pady=8)
        self._gap(note, 4)

    # ─── Memory ──────────────────────────────────────────
    def _render_memory(self, data):
        self._clr(self._mem_scroll)
        f = self._mem_scroll
        card = self._card(f, "Utilizare RAM")
        self._pbar(card, data.get('pct',0),
                   f"RAM  {fmt_bytes(data.get('used',0))} / {fmt_bytes(data.get('total',0))}")
        self._row(card, "Total",      fmt_bytes(data.get('total',0)))
        self._row(card, "Utilizat",   fmt_bytes(data.get('used',0)))
        self._row(card, "Disponibil", fmt_bytes(data.get('available',0)))
        if data.get('swap_total',0):
            self._pbar(card, data.get('swap_pct',0),
                       f"Swap  {fmt_bytes(data.get('swap_used',0))}/{fmt_bytes(data.get('swap_total',0))}")
        self._gap(card, 6)
        slots = data.get('slots', [])
        if slots:
            card2 = self._card(f, f"Module RAM fizice ({len(slots)} detectat(e))")
            for i, sl in enumerate(slots):
                ctk.CTkLabel(card2, text=f"  Slot: {sl['slot']}",
                             font=ctk.CTkFont(size=12, weight='bold'),
                             text_color='#4f8ef7', anchor='w').pack(anchor='w', padx=14, pady=(8,0))
                self._row(card2, "Capacitate",  fmt_bytes(sl['cap']))
                self._row(card2, "Tip",         sl['type'])
                self._row(card2, "Viteza",      f"{sl['speed']} MHz")
                self._row(card2, "Producator",  sl['mfr'])
                self._row(card2, "Part Number", sl['part'])
                if i < len(slots)-1:
                    ctk.CTkFrame(card2, height=1, fg_color='#1e293b').pack(fill='x', padx=14, pady=4)
            self._gap(card2, 6)

    # ─── GPU ─────────────────────────────────────────────
    def _render_gpu(self, data):
        self._clr(self._gpu_scroll)
        f    = self._gpu_scroll
        gpus = data.get('gpus', [])
        nv   = data.get('nvidia', [])
        lhm  = data.get('lhm_temps', {})

        if not gpus:
            ce = self._card(f, "Placi video detectate")
            ctk.CTkLabel(ce, text=(
                "  Nu s-a detectat nicio placa video prin WMI sau PowerShell.\n"
                "  Posibile cauze: drepturi insuficiente, driver neinstalat, VM."
            ), text_color='#475569', font=ctk.CTkFont(size=11), anchor='w',
               justify='left').pack(padx=14, pady=10)
            self._gap(ce, 4)
            # Arata totusi datele nvidia-smi daca exista
            if nv:
                cn = self._card(f, "NVIDIA (via nvidia-smi)")
                for n in nv:
                    ctk.CTkLabel(cn, text=f"  {n['name']}",
                                 font=ctk.CTkFont(size=13, weight='bold'),
                                 text_color='#76b900', anchor='w').pack(anchor='w', padx=14, pady=(8,0))
                    try:
                        temp = float(n['temp'])
                        th = health_of(temp, 'gpu_temp')
                        self._row(cn, "Temperatura", f"{temp:.0f}°C", self._hc(th))
                    except Exception: pass
                    self._row(cn, "Utilizare GPU", f"{n['gpu_u']}%")
                    self._row(cn, "VRAM", f"{n['mem_used']}/{n['mem_tot']} MB")
                    if n['power'] != 'N/A':
                        pw = f"{n['power']} W"
                        if n.get('plimit','N/A') != 'N/A': pw += f" / {n['plimit']} W limita"
                        self._row(cn, "Consum", pw)
                self._gap(cn, 6)
            return

        for i, g in enumerate(gpus):
            col  = self._hc(g['health'])
            card = self._card(f, f"GPU {i+1}: {g['name']}", border=col)

            # Badge stare + sursa detectare
            br = ctk.CTkFrame(card, fg_color='transparent')
            br.pack(fill='x', padx=14, pady=(2, 8))
            ctk.CTkLabel(br, text="Stare: ", text_color='gray',
                         font=ctk.CTkFont(size=12)).pack(side='left')
            self._badge(br, g['health'], g['status'])
            src = g.get('source', '')
            if src:
                ctk.CTkLabel(br, text=f"  [{src}]", text_color='#334155',
                             font=ctk.CTkFont(size=10)).pack(side='left', padx=4)

            # Tip GPU (NVIDIA / AMD / Intel Iris)
            nm_low = g['name'].lower()
            if 'nvidia' in nm_low or 'geforce' in nm_low or 'quadro' in nm_low or 'rtx' in nm_low or 'gtx' in nm_low:
                gpu_vendor = 'NVIDIA'
                vcol = '#76b900'
            elif 'amd' in nm_low or 'radeon' in nm_low or 'rx ' in nm_low:
                gpu_vendor = 'AMD'
                vcol = '#ED1C24'
            elif 'intel' in nm_low or 'iris' in nm_low or 'uhd' in nm_low or 'hd graphics' in nm_low:
                gpu_vendor = 'Intel'
                vcol = '#0071C5'
            else:
                gpu_vendor = ''
                vcol = '#94a3b8'
            if gpu_vendor:
                ctk.CTkLabel(br, text=f"  {gpu_vendor}", text_color=vcol,
                             font=ctk.CTkFont(size=11, weight='bold')).pack(side='left', padx=4)

            if g['vram']:
                self._row(card, "VRAM", fmt_bytes(g['vram']))
            self._row(card, "Driver",      g['driver'])
            self._row(card, "Data driver", g['dd'])
            if g['res'] not in ('0x0', '0x', 'x', ''):
                self._row(card, "Rezolutie",   g['res'])
            if g['hz']:
                self._row(card, "Rata refresh", f"{g['hz']} Hz")

            # NVIDIA live via nvidia-smi
            if nv and i < len(nv):
                n = nv[i]
                ctk.CTkLabel(card, text="  Live (nvidia-smi):", text_color='#475569',
                             font=ctk.CTkFont(size=11), anchor='w').pack(anchor='w', padx=14, pady=(8,0))
                try:
                    temp = float(n['temp'])
                    th   = health_of(temp, 'gpu_temp')
                    self._row(card, "Temperatura", f"{temp:.0f}°C", self._hc(th))
                except Exception: pass
                self._row(card, "Utilizare GPU",    f"{n['gpu_u']}%")
                self._row(card, "Memorie folosita", f"{n['mem_used']}/{n['mem_tot']} MB")
                if n['power'] != 'N/A':
                    p_txt = f"{n['power']} W"
                    if n.get('plimit','N/A') != 'N/A': p_txt += f" / {n['plimit']} W limita"
                    self._row(card, "Consum", p_txt)
                if n.get('clk','N/A') != 'N/A':
                    self._row(card, "Clock GPU", f"{n['clk']} MHz")

            # AMD/Intel temperaturi din LHM
            elif lhm:
                nm_low2 = g['name'].lower()
                matched = {k: v for k, v in lhm.items()
                           if any(w in k.lower() for w in nm_low2.split()[:3])}
                if not matched:
                    matched = lhm  # afiseaza tot daca nu se poate asocia
                if matched:
                    ctk.CTkLabel(card, text="  Live (LibreHardwareMonitor):", text_color='#475569',
                                 font=ctk.CTkFont(size=11), anchor='w').pack(anchor='w', padx=14, pady=(8,0))
                    for sname, sval in matched.items():
                        th = health_of(sval, 'gpu_temp')
                        self._row(card, sname, f"{sval}°C", self._hc(th))

            self._gap(card, 6)

        # Info temperaturi daca nu exista sursa live
        if not nv and not lhm:
            ci = self._card(f, "Temperaturi GPU")
            ctk.CTkLabel(ci, text=(
                "  Temperatura GPU nu este disponibila.\n"
                "  Pentru AMD/Intel: porniti LibreHardwareMonitor.\n"
                "  Pentru NVIDIA: asigurati-va ca nvidia-smi este in PATH."
            ), text_color='#475569', font=ctk.CTkFont(size=11), anchor='w',
               justify='left').pack(padx=14, pady=(6,4))
            self._gap(ci, 4)

    # ─── Network ─────────────────────────────────────────
    def _render_network(self, data):
        self._clr(self._net_scroll)
        f = self._net_scroll
        adapters = data.get('adapters', [])
        addrs    = data.get('addrs', {})
        stats    = data.get('stats', {})

        card = self._card(f, "Adaptoare de retea active")
        if not adapters:
            ctk.CTkLabel(card, text="  Niciun adaptor fizic detectat.",
                         text_color='gray').pack(padx=14, pady=8)
        else:
            for nic in adapters:
                ctk.CTkLabel(card, text=f"  {nic['name']}",
                             font=ctk.CTkFont(size=12, weight='bold'),
                             text_color='#06b6d4', anchor='w').pack(anchor='w', padx=14, pady=(8,0))
                self._row(card, "Tip",      nic['type'])
                self._row(card, "MAC",      nic['mac'])
                if nic['speed']:
                    spd = nic['speed']
                    if spd >= 1_000_000_000: s = f"{spd/1_000_000_000:.0f} Gbps"
                    elif spd >= 1_000_000:   s = f"{spd/1_000_000:.0f} Mbps"
                    else: s = f"{spd} bps"
                    self._row(card, "Viteza legatura", s)
        self._gap(card, 6)

        if addrs:
            card2 = self._card(f, "Adrese IP")
            for iface, ips in addrs.items():
                st = stats.get(iface, {})
                status = 'Activ' if st.get('up') else 'Inactiv'
                col    = '#22c55e' if st.get('up') else '#6b7280'
                ctk.CTkLabel(card2, text=f"  {iface}",
                             font=ctk.CTkFont(size=12, weight='bold'),
                             text_color=col, anchor='w').pack(anchor='w', padx=14, pady=(8,0))
                self._row(card2, "Status", status, col)
                for ip in ips:
                    self._row(card2, "IP", ip)
                sp = st.get('speed', 0)
                if sp: self._row(card2, "Viteza", f"{sp} Mbps")
            self._gap(card2, 6)

        # Gateway + DNS
        gateways = data.get('gateways', [])
        if gateways:
            cg = self._card(f, "Gateway si DNS")
            for gw in gateways:
                ctk.CTkLabel(cg, text=f"  {gw.get('iface','?')}",
                             font=ctk.CTkFont(size=12, weight='bold'),
                             text_color='#8b5cf6', anchor='w').pack(anchor='w', padx=14, pady=(8,0))
                self._row(cg, "Gateway", gw.get('gateway','?'))
                dns = gw.get('dns','')
                if dns: self._row(cg, "DNS", dns)
            self._gap(cg, 6)

        # Internet + latency
        internet = data.get('internet')
        latency  = data.get('latency_ms', -1)
        ci = self._card(f, "Conectivitate internet")
        bri = ctk.CTkFrame(ci, fg_color='transparent')
        bri.pack(fill='x', padx=14, pady=(2, 8))
        if internet is None:
            self._badge(bri, 'unknown', 'Necunoscut')
        elif internet:
            self._badge(bri, 'good', 'Online')
            if latency and latency > 0:
                lat_h = 'good' if latency < 50 else ('warning' if latency < 150 else 'bad')
                self._row(ci, "Latenta (8.8.8.8)", f"{latency} ms", self._hc(lat_h))
        else:
            self._badge(bri, 'bad', 'Offline — fara acces la internet')
        self._gap(ci, 4)

    # ─── Security ────────────────────────────────────────
    def _render_security(self, sec, net=None):
        self._clr(self._sec_scroll)
        f = self._sec_scroll

        # Windows Defender
        df = sec.get('defender', {})
        if df.get('available'):
            en  = df.get('enabled', False)
            rt  = df.get('rt_protection', False)
            age = df.get('sig_age_days', 0)
            dh = ('good' if (en and rt and age <= 7) else
                  'warning' if (en and rt and age <= 30) else 'bad')
            cd = self._card(f, "Windows Defender", border=self._hc(dh))
            brd = ctk.CTkFrame(cd, fg_color='transparent')
            brd.pack(fill='x', padx=14, pady=(2, 8))
            self._badge(brd, 'good' if en else 'bad', 'Activ' if en else 'Inactiv')
            self._badge(brd, 'good' if rt else 'bad',
                        'Protectie in timp real ON' if rt else 'Protectie in timp real OFF')
            age_h = 'good' if age <= 7 else ('warning' if age <= 30 else 'bad')
            self._row(cd, "Semnatura definitii", f"{age} zile", self._hc(age_h))
            if df.get('last_scan','N/A') != 'N/A':
                self._row(cd, "Ultima scanare", df['last_scan'])
            if df.get('version','N/A') != 'N/A':
                self._row(cd, "Versiune motor", df['version'])
            self._gap(cd, 6)
        else:
            cd = self._card(f, "Windows Defender")
            ctk.CTkLabel(cd, text="  Defender nu a putut fi citit (necesita drepturi de administrator).",
                         text_color='#475569', font=ctk.CTkFont(size=11)).pack(padx=14, pady=8)
            self._gap(cd, 4)

        # Firewall
        fw = sec.get('firewall', [])
        if fw:
            n_on = sum(1 for x in fw if x.get('enabled'))
            fw_h = 'good' if n_on == len(fw) else ('warning' if n_on > 0 else 'bad')
            cfw = self._card(f, "Windows Firewall", border=self._hc(fw_h))
            for profile in fw:
                ph  = 'good' if profile.get('enabled') else 'bad'
                brf = ctk.CTkFrame(cfw, fg_color='transparent')
                brf.pack(fill='x', padx=14, pady=2)
                ctk.CTkLabel(brf, text=profile.get('name','?'), width=100, anchor='w',
                             text_color='#94a3b8', font=ctk.CTkFont(size=12)).pack(side='left')
                self._badge(brf, ph, 'Activ' if profile.get('enabled') else 'Inactiv')
            self._gap(cfw, 6)

        # Activare Windows
        act = sec.get('activation', 'Necunoscut')
        act_h = 'good' if act == 'Activat' else ('bad' if act == 'Nelicentiat' else 'unknown')
        ca = self._card(f, "Activare Windows", border=self._hc(act_h))
        bra = ctk.CTkFrame(ca, fg_color='transparent')
        bra.pack(fill='x', padx=14, pady=(2, 8))
        self._badge(bra, act_h, act)
        self._gap(ca, 4)

        # Remote Desktop
        rdp = sec.get('rdp', 'Necunoscut')
        rdp_h = 'warning' if rdp == 'Activat' else ('good' if rdp == 'Dezactivat' else 'unknown')
        cr = self._card(f, "Remote Desktop (RDP)", border=self._hc(rdp_h))
        brr = ctk.CTkFrame(cr, fg_color='transparent')
        brr.pack(fill='x', padx=14, pady=(2, 8))
        self._badge(brr, rdp_h, rdp)
        if rdp == 'Activat':
            ctk.CTkLabel(cr, text="  Atentie: RDP activ poate fi un risc de securitate daca nu este necesar.",
                         text_color=HC['warning'], font=ctk.CTkFont(size=11)).pack(padx=14, pady=(0, 8))
        self._gap(cr, 4)

        # BitLocker
        bl = sec.get('bitlocker', [])
        if bl:
            cb = self._card(f, "BitLocker Encryption")
            for vol in bl:
                ps  = str(vol.get('protection', ''))
                protected = 'on' in ps.lower() or ps == '1'
                ph  = 'good' if protected else 'warning'
                bbl = ctk.CTkFrame(cb, fg_color='transparent')
                bbl.pack(fill='x', padx=14, pady=3)
                ctk.CTkLabel(bbl, text=vol.get('mount','?'), width=60, anchor='w',
                             font=ctk.CTkFont(size=12, weight='bold')).pack(side='left')
                self._badge(bbl, ph, 'Protejat' if protected else 'Neprotejat')
                ctk.CTkLabel(bbl, text=f"  {vol.get('status','')}",
                             text_color='#64748b', font=ctk.CTkFont(size=11)).pack(side='left', padx=6)
            self._gap(cb, 6)

        # Administratori locali
        admins = sec.get('admins', [])
        if admins:
            cad = self._card(f, f"Administratori locali ({len(admins)})")
            src_map = {'1': 'Local', '2': 'Active Directory', '4': 'Cont Microsoft', '0': '?'}
            for adm in admins:
                src_txt = src_map.get(str(adm.get('source','')), str(adm.get('source','')))
                self._row(cad, adm.get('name','?'), src_txt)
            self._gap(cad, 6)

        # Ultimele actualizari
        updates = sec.get('updates', [])
        if updates:
            cu = self._card(f, "Ultimele actualizari Windows instalate")
            for upd in updates:
                r = ctk.CTkFrame(cu, fg_color='transparent')
                r.pack(fill='x', padx=14, pady=2)
                ctk.CTkLabel(r, text=upd.get('id','?'), width=110, anchor='w',
                             font=ctk.CTkFont(size=11, weight='bold', family='Consolas'),
                             text_color='#4f8ef7').pack(side='left')
                ctk.CTkLabel(r, text=upd.get('date',''), width=100, anchor='w',
                             text_color='#64748b', font=ctk.CTkFont(size=11)).pack(side='left')
                ctk.CTkLabel(r, text=upd.get('desc',''), anchor='w',
                             text_color='#94a3b8', font=ctk.CTkFont(size=11)).pack(side='left', padx=4)
            self._gap(cu, 6)
        else:
            cu2 = self._card(f, "Actualizari Windows")
            ctk.CTkLabel(cu2, text="  Necesita drepturi de administrator pentru citire.",
                         text_color='#475569', font=ctk.CTkFont(size=11)).pack(padx=14, pady=8)
            self._gap(cu2, 4)

    # ─── System ──────────────────────────────────────────
    def _render_system(self, data, fans=None, perf_extras=None, security=None):
        fans        = fans or []
        perf_extras = perf_extras or {}
        security    = security or {}
        self._clr(self._sys_scroll)
        f = self._sys_scroll
        cpu   = data.get('cpu', {})
        freq  = data.get('freq', {})
        temps = data.get('temps', [])

        card = self._card(f, "Procesor (CPU)")
        self._pbar(card, cpu.get('usage',0), "Utilizare CPU")
        self._row(card, "Model", cpu.get('name','N/A'))
        self._row(card, "Nuclee fizice / logice",
                  f"{cpu.get('pc','?')} / {cpu.get('lc','?')}")
        if freq:
            self._row(card, "Frecventa curenta", f"{freq.get('cur',0):.0f} MHz")
            self._row(card, "Frecventa maxima",  f"{freq.get('max',0):.0f} MHz")
        if 'socket' in cpu: self._row(card, "Socket", cpu['socket'])
        if temps:
            avg = sum(temps)/len(temps)
            self._row(card, "Temperatura (ACPI)", f"{avg:.1f}°C", self._hc(health_of(avg,'cpu_temp')))
        else:
            self._row(card, "Temperatura", "Indisponibila pe acest sistem", '#475569')
        self._gap(card, 6)

        mb = data.get('mb', {})
        if mb:
            c2 = self._card(f, "Placa de baza")
            self._row(c2, "Producator", mb.get('mfr','N/A'))
            self._row(c2, "Model",      mb.get('model','N/A'))
            self._row(c2, "Versiune",   mb.get('ver','N/A'))
            self._row(c2, "Serial",     mb.get('serial','N/A'))
            self._gap(c2, 6)

        bios = data.get('bios', {})
        if bios:
            c3 = self._card(f, "BIOS / UEFI")
            self._row(c3, "Producator",    bios.get('mfr','N/A'))
            self._row(c3, "Versiune BIOS", bios.get('ver','N/A'))
            self._row(c3, "Data release",  bios.get('date','N/A'))
            self._gap(c3, 6)

        os_d = data.get('os', {})
        if os_d:
            c4 = self._card(f, "Sistem de operare")
            self._row(c4, "Nume",        os_d.get('name','N/A'))
            self._row(c4, "Versiune",    os_d.get('ver','N/A'))
            self._row(c4, "Build",       os_d.get('build','N/A'))
            self._row(c4, "Arhitectura", os_d.get('arch','N/A'))
            if os_d.get('install'): self._row(c4, "Data instalare", os_d['install'])
            self._gap(c4, 6)

        if data.get('uptime'):
            c5 = self._card(f, "Functionare")
            self._row(c5, "Pornit de", data['uptime'])
            if data.get('boot'): self._row(c5, "Ultima pornire", data['boot'])
            self._gap(c5, 6)

        # Fans card
        if fans:
            src = fans[0].get('source', '') if fans else ''
            cf  = self._card(f, f"Ventilatoare ({len(fans)} detectate)  —  {src}")
            for fan in fans:
                rpm = fan.get('rpm', 0)
                nm  = fan.get('name', 'Fan')
                if fan.get('parent'):
                    nm = f"{fan['parent']} › {nm}"
                col = HC['good'] if rpm > 600 else (HC['warning'] if rpm > 0 else '#475569')
                self._row(cf, nm, f"{rpm:,} RPM", col)
            self._gap(cf, 6)
        else:
            cf = self._card(f, "Ventilatoare")
            ctk.CTkLabel(cf, text=(
                "  Viteza ventilatoarelor nu este accesibila fara software dedicat.\n"
                "  Porniti LibreHardwareMonitor sau OpenHardwareMonitor pentru date live."
            ), text_color='#475569', font=ctk.CTkFont(size=11), anchor='w',
               justify='left').pack(padx=14, pady=(6, 4))
            for nm, desc in [
                ("LibreHardwareMonitor", "github.com/LibreHardwareMonitor — open-source, fara instalare"),
                ("HWiNFO64",             "hwinfo.com — cel mai complet, suporta toti senzorii"),
            ]:
                r = ctk.CTkFrame(cf, fg_color='transparent')
                r.pack(fill='x', padx=14, pady=2)
                ctk.CTkLabel(r, text="•", text_color=HC['warning'], width=18).pack(side='left')
                ctk.CTkLabel(r, text=nm, font=ctk.CTkFont(size=12, weight='bold'),
                             width=210).pack(side='left')
                ctk.CTkLabel(r, text=desc, text_color='#64748b',
                             font=ctk.CTkFont(size=11)).pack(side='left', padx=4)
            self._gap(cf, 6)

        # ── Activare Windows ─────────────────────────────
        act = security.get('activation', '')
        if act:
            act_h = 'good' if act == 'Activat' else ('bad' if act == 'Nelicentiat' else 'unknown')
            cact = self._card(f, "Activare Windows", border=self._hc(act_h))
            bra = ctk.CTkFrame(cact, fg_color='transparent')
            bra.pack(fill='x', padx=14, pady=(2, 8))
            self._badge(bra, act_h, act)
            self._gap(cact, 4)

        # ── Ultimele actualizari ──────────────────────────
        updates = security.get('updates', [])
        if updates:
            cu = self._card(f, "Ultimele actualizari Windows instalate")
            for upd in updates:
                r = ctk.CTkFrame(cu, fg_color='transparent')
                r.pack(fill='x', padx=14, pady=2)
                ctk.CTkLabel(r, text=upd.get('id','?'), width=110, anchor='w',
                             font=ctk.CTkFont(size=11, weight='bold', family='Consolas'),
                             text_color='#4f8ef7').pack(side='left')
                ctk.CTkLabel(r, text=upd.get('date',''), width=100, anchor='w',
                             text_color='#64748b', font=ctk.CTkFont(size=11)).pack(side='left')
                ctk.CTkLabel(r, text=upd.get('desc',''), anchor='w',
                             text_color='#94a3b8', font=ctk.CTkFont(size=11)).pack(side='left', padx=4)
            self._gap(cu, 6)

        # ── Programe la pornire ───────────────────────────
        startup = perf_extras.get('startup', [])
        if startup:
            cs = self._card(f, f"Programe la pornire ({len(startup)})")
            for prog in startup:
                r = ctk.CTkFrame(cs, fg_color='transparent')
                r.pack(fill='x', padx=14, pady=2)
                ctk.CTkLabel(r, text=prog.get('name','?')[:32], width=200, anchor='w',
                             font=ctk.CTkFont(size=11, weight='bold')).pack(side='left')
                ctk.CTkLabel(r, text=prog.get('user',''), width=110, anchor='w',
                             text_color='#64748b', font=ctk.CTkFont(size=11)).pack(side='left')
                ctk.CTkLabel(r, text=prog.get('command','')[:60], anchor='w',
                             text_color='#475569',
                             font=ctk.CTkFont(size=10, family='Consolas')).pack(side='left', padx=4)
            self._gap(cs, 6)

        # ── Boot time ────────────────────────────────────
        boot_ms = perf_extras.get('boot_ms')
        if boot_ms is not None and boot_ms > 0:
            bt_s = boot_ms / 1000
            bt_h = 'good' if bt_s < 30 else ('warning' if bt_s < 60 else 'bad')
            cbt = self._card(f, "Durata boot", border=self._hc(bt_h))
            brb = ctk.CTkFrame(cbt, fg_color='transparent')
            brb.pack(fill='x', padx=14, pady=(2, 8))
            self._badge(brb, bt_h, f"{bt_s:.1f} secunde")
            self._gap(cbt, 4)

        # ── Evenimente critice ────────────────────────────
        events = perf_extras.get('events', [])
        if events:
            ce = self._card(f, f"Evenimente critice / erori (ultimele 7 zile — {len(events)} gasite)",
                            border=HC['warning'])
            for ev in events:
                r = ctk.CTkFrame(ce, fg_color='transparent')
                r.pack(fill='x', padx=14, pady=2)
                ctk.CTkLabel(r, text=str(ev.get('time',''))[:16], width=130, anchor='w',
                             text_color='#64748b', font=ctk.CTkFont(size=11)).pack(side='left')
                ctk.CTkLabel(r, text=f"ID:{ev.get('id',0)}", width=75, anchor='w',
                             text_color=HC['bad'],
                             font=ctk.CTkFont(size=11, weight='bold', family='Consolas')).pack(side='left')
                ctk.CTkLabel(r, text=ev.get('msg','')[:70], anchor='w',
                             text_color='#94a3b8', font=ctk.CTkFont(size=10)).pack(side='left', padx=4)
            self._gap(ce, 6)
        else:
            ce2 = self._card(f, "Evenimente critice / erori (ultimele 7 zile)", border=HC['good'])
            ctk.CTkLabel(ce2, text="  Nicio eroare critica detectata in ultimele 7 zile.",
                         text_color=HC['good'], font=ctk.CTkFont(size=11)).pack(padx=14, pady=8)
            self._gap(ce2, 4)

    # ─── PSU ─────────────────────────────────────────────
    def _render_psu(self, data):
        self._clr(self._psu_scroll)
        f = self._psu_scroll
        if data.get('type') == 'laptop':
            pct    = data.get('pct', 0)
            plugged = data.get('plugged', False)
            secs   = data.get('secs', 0)
            h = 'good' if plugged else ('bad' if pct<15 else ('warning' if pct<35 else 'good'))
            status = ('Conectat la curent' if plugged else
                      'Baterie critica!' if pct<15 else
                      'Baterie descarcata' if pct<35 else 'Pe baterie')
            col  = self._hc(h)
            card = self._card(f, "Baterie laptop", border=col)
            br   = ctk.CTkFrame(card, fg_color='transparent')
            br.pack(fill='x', padx=14, pady=(2,8))
            ctk.CTkLabel(br, text="Stare: ", text_color='gray', font=ctk.CTkFont(size=12)).pack(side='left')
            self._badge(br, h, status)
            self._pbar(card, pct, "Nivel incarcare")
            if secs > 0 and not plugged:
                m = secs // 60
                self._row(card, "Timp ramas", f"{m//60}h {m%60}min")
            # Battery health bar
            hp = data.get('health_pct')
            if hp is not None:
                hh   = 'good' if hp >= 80 else ('warning' if hp >= 60 else 'bad')
                hcol = self._hc(hh)
                hr   = ctk.CTkFrame(card, fg_color='transparent')
                hr.pack(fill='x', padx=14, pady=3)
                ctk.CTkLabel(hr, text="Sanatate baterie", width=260, anchor='w',
                             text_color='#64748b', font=ctk.CTkFont(size=12)).pack(side='left')
                hbar = ctk.CTkProgressBar(hr, height=13)
                hbar.set(max(0.0, min(1.0, hp / 100)))
                hbar.configure(progress_color=hcol)
                hbar.pack(side='left', fill='x', expand=True, padx=(0, 8))
                ctk.CTkLabel(hr, text=f'{hp:.1f}%', width=52,
                             font=ctk.CTkFont(size=12)).pack(side='left')
                dc = data.get('design_cap')
                fc = data.get('full_cap')
                if dc and fc:
                    self._row(card, "Capacitate originala", f"{dc:,} mWh")
                    self._row(card, "Capacitate actuala",   f"{fc:,} mWh")
                    lost = dc - fc
                    if lost > 0:
                        self._row(card, "Capacitate pierduta",
                                  f"{lost:,} mWh  ({100 - hp:.1f}% uzura)",
                                  hcol if hh != 'good' else '#475569')
            self._gap(card, 6)
        else:
            # ── Consum energie sistem (RAPL) ────────────
            rapl = data.get('rapl', [])
            if rapl:
                total_w = sum(x['watts'] for x in rapl)
                cr = self._card(f, "Consum energie sistem (RAPL)",
                                border=HC['good'] if total_w < 200 else HC['warning'])
                for entry in sorted(rapl, key=lambda x: -x['watts']):
                    wh = ('good' if entry['watts'] < 100 else
                          'warning' if entry['watts'] < 250 else 'bad')
                    self._row(cr, entry['name'].capitalize(), f"{entry['watts']:.1f} W",
                              self._hc(wh))
                ctk.CTkFrame(cr, height=1, fg_color='#1e293b').pack(fill='x', padx=12, pady=4)
                tot_h = 'good' if total_w < 200 else ('warning' if total_w < 400 else 'bad')
                self._row(cr, "Total masurat", f"{total_w:.1f} W", self._hc(tot_h))
                self._gap(cr, 4)
            else:
                cr = self._card(f, "Consum energie sistem (RAPL)")
                ctk.CTkLabel(cr, text=(
                    "  RAPL (Running Average Power Limit) nu este suportat pe acest sistem.\n"
                    "  Disponibil pe Intel Sandy Bridge+, AMD Zen 3+ cu driver energy meter."
                ), text_color='#475569', font=ctk.CTkFont(size=11), anchor='w',
                   justify='left').pack(padx=14, pady=(6, 8))
                self._gap(cr, 2)

            # ── Tensiune CPU (VCore) ──────────────────────
            vcores = data.get('vcores', [])
            if vcores:
                cv = self._card(f, "Tensiune CPU (VCore)")
                for vc in vcores:
                    v   = vc['voltage']
                    vh  = 'good' if 0.8 <= v <= 1.5 else ('warning' if 0.6 <= v <= 1.7 else 'bad')
                    self._row(cv, vc['name'][:45], f"{v:.2f} V", self._hc(vh))
                self._gap(cv, 4)

            # ── Putere GPU (nvidia-smi) ───────────────────
            gpu_d  = self._data.get('gpu', {}) if self._data else {}
            nvidia = gpu_d.get('nvidia', [])
            if nvidia:
                cpw = self._card(f, "Putere GPU (nvidia-smi)")
                for n in nvidia:
                    if n.get('power','N/A') != 'N/A':
                        try:
                            pw = float(n['power'])
                            ph = 'good' if pw < 100 else ('warning' if pw < 200 else 'bad')
                            nm = n.get('name', 'GPU')
                            pw_txt = f"{pw:.0f} W"
                            if n.get('plimit','N/A') != 'N/A':
                                pw_txt += f"  /  {n['plimit']} W limita"
                            self._row(cpw, nm, pw_txt, self._hc(ph))
                        except Exception: pass
                self._gap(cpw, 4)

            # ── Plan de alimentare Windows ────────────────
            plan = data.get('power_plan', '')
            if plan and plan != 'Necunoscut':
                pp = plan.lower()
                if 'performance' in pp or 'performanta' in pp:
                    plan_h, plan_note = 'warning', 'Consum maxim — recomandat numai la desktop'
                elif 'power saver' in pp or 'economisire' in pp or 'saver' in pp:
                    plan_h, plan_note = 'warning', 'Performanta redusa — nu recomandat la desktop'
                else:
                    plan_h, plan_note = 'good', 'Echilibrat intre performanta si consum'
                cp2 = self._card(f, "Plan de alimentare Windows", border=self._hc(plan_h))
                br2 = ctk.CTkFrame(cp2, fg_color='transparent')
                br2.pack(fill='x', padx=14, pady=(2, 4))
                self._badge(br2, plan_h, plan)
                ctk.CTkLabel(cp2, text=f"  {plan_note}", text_color='#64748b',
                             font=ctk.CTkFont(size=11)).pack(anchor='w', padx=14, pady=(0, 8))
                self._gap(cp2, 2)

            # ── Despre tensiunile sursei ──────────────────
            ci = self._card(f, "Tensiuni sursa (+12V / +5V / +3.3V)")
            ctk.CTkLabel(ci, text=(
                "  Tensiunile rail-urilor sursei (+12V, +5V, +3.3V) necesita acces\n"
                "  la senzorii hardware ai placii de baza (via driver kernel).\n"
                "  Windows nu expune aceste valori prin API-uri standard.\n\n"
                "  Alternativa: HWiNFO64 sau HWMonitor (gratuite, fara instalare)."
            ), text_color='#475569', font=ctk.CTkFont(size=11), anchor='w',
               justify='left').pack(padx=14, pady=(6, 8))
            self._gap(ci, 2)

            # ── Sfaturi intretinere ───────────────────────
            ct = self._card(f, "Sfaturi intretinere sursa")
            for tip in [
                "Curatati praful din sursa la fiecare 6-12 luni cu aer comprimat",
                "Nu supraincarcati sursa — lasati 20-30% putere de rezerva",
                "Folositi un UPS sau protector de tensiune",
                "Durata medie de viata a unei surse de calitate: 5-10 ani",
                "Miros ars sau zgomot neobisnuit = semnale de alarma imediate",
                "Regula dimensionare: CPU TDP + GPU TDP + 100W (restul) + 30% rezerva",
            ]:
                r = ctk.CTkFrame(ct, fg_color='transparent')
                r.pack(fill='x', padx=14, pady=2)
                ctk.CTkLabel(r, text="•", text_color=HC['warning'], width=18).pack(side='left')
                ctk.CTkLabel(r, text=tip, text_color='#94a3b8',
                             font=ctk.CTkFont(size=12)).pack(side='left')
            self._gap(ct, 6)

    # ─── Auto-refresh ────────────────────────────────────
    def _schedule_ar(self):
        if self._ar_id:
            self.after_cancel(self._ar_id)
            self._ar_id = None
        val = self._ar_var.get()
        if val == 'Manual': return
        secs = int(val.rstrip('s'))
        self._ar_id = self.after(secs * 1000, self._auto_ar)

    def _auto_ar(self):
        self._do_refresh()

    # ─── Export HTML ─────────────────────────────────────
    def _export_html(self):
        if not self._data:
            return
        path = filedialog.asksaveasfilename(
            title="Salveaza raportul HTML",
            defaultextension=".html",
            filetypes=[("HTML", "*.html"), ("Toate fisierele", "*.*")],
            initialfile=f"pc_health_{datetime.now().strftime('%Y%m%d_%H%M%S')}.html"
        )
        if not path: return
        if not path.lower().endswith(('.html', '.htm')):
            path += '.html'
        try:
            self._exp.export(self._data, self._store, path)
            webbrowser.open(path)
        except Exception as e:
            tk.messagebox.showerror("Eroare export HTML", str(e), parent=self)

    # ─── Export JSON ─────────────────────────────────────
    def _export_json(self):
        if not self._data:
            return
        path = filedialog.asksaveasfilename(
            title="Salveaza raportul JSON",
            defaultextension=".json",
            filetypes=[("JSON", "*.json"), ("Toate fisierele", "*.*")],
            initialfile=f"pc_health_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
        )
        if not path: return
        if not path.lower().endswith('.json'):
            path += '.json'
        try:
            tmp = path + '.tmp'
            with open(tmp, 'w', encoding='utf-8') as fh:
                json.dump(self._data, fh, ensure_ascii=False, indent=2, default=str)
            os.replace(tmp, path)
            tk.messagebox.showinfo("Export JSON", f"Salvat:\n{path}", parent=self)
        except Exception as e:
            tk.messagebox.showerror("Eroare export JSON", str(e), parent=self)

    # ─── Export PDF ──────────────────────────────────────
    def _export_pdf(self):
        if not self._data:
            return
        path = filedialog.asksaveasfilename(
            title="Salveaza raportul PDF",
            defaultextension=".pdf",
            filetypes=[("PDF", "*.pdf"), ("Toate fisierele", "*.*")],
            initialfile=f"pc_health_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"
        )
        if not path: return
        if not path.lower().endswith('.pdf'):
            path += '.pdf'
        try:
            self._pdf_exp.export(self._data, path)
            os.startfile(path)
        except Exception as e:
            tk.messagebox.showerror("Eroare export PDF", str(e), parent=self)

    # ─── Cloud Sync ───────────────────────────────────────
    def _cloud_settings_path(self):
        base = os.environ.get('LOCALAPPDATA', os.path.expanduser('~'))
        return os.path.join(base, 'PCHealthMonitor', 'cloud_settings.json')

    _CLOUD_DEFAULTS = {
        'url': 'https://ut.rootsecurity.ro',
        'key': 'pchm-bePwavX76lFgBurp8szyKYnJidqRI254',
    }

    def _cloud_settings(self) -> dict:
        try:
            with open(self._cloud_settings_path(), 'r', encoding='utf-8') as f:
                d = json.load(f)
            if isinstance(d, dict) and d.get('url') and d.get('key'):
                return d
        except Exception:
            pass
        return self._CLOUD_DEFAULTS

    def _cloud_save_settings(self, url: str, key: str):
        path = self._cloud_settings_path()
        os.makedirs(os.path.dirname(path), exist_ok=True)
        tmp = path + '.tmp'
        with open(tmp, 'w', encoding='utf-8') as f:
            json.dump({'url': url.strip(), 'key': key.strip()}, f)
        os.replace(tmp, path)

    @staticmethod
    def _ssl_ctx():
        import ssl
        ctx = ssl.create_default_context()
        ctx.check_hostname = False
        ctx.verify_mode = ssl.CERT_NONE
        return ctx

    def _cloud_sync(self, data: dict):
        """Trimite datele la serverul cloud (daca e configurat)."""
        cfg = self._cloud_settings()
        url = cfg.get('url', '').strip()
        key = cfg.get('key', '').strip()
        if not url or not key:
            return
        try:
            score, grade, _ = _calc_health_score(data)
            payload = dict(data)
            payload['_health_score'] = score
            payload['_health_grade'] = grade
            body = json.dumps(payload, default=str).encode('utf-8')
            import urllib.request, urllib.error
            req = urllib.request.Request(
                url.rstrip('/') + '/api/v1/report',
                data=body,
                headers={'Content-Type': 'application/json',
                         'X-API-Key': key},
                method='POST')
            with urllib.request.urlopen(req, timeout=15, context=self._ssl_ctx()) as resp:
                resp.read()
            self.after(0, lambda: self._cloud_lbl.configure(
                text='☁ Sincronizat', text_color='#00C853'))
        except Exception as e:
            err = str(e)[:35]
            self.after(0, lambda: self._cloud_lbl.configure(
                text=f'☁ {err}', text_color='#FF5252'))

    def _show_cloud_dialog(self):
        dlg = ctk.CTkToplevel(self)
        dlg.title("Cloud Sync — Setari")
        dlg.geometry("500x320")
        dlg.resizable(False, False)
        dlg.attributes('-topmost', True)
        dlg.grab_set()
        cfg = self._cloud_settings()

        ctk.CTkLabel(dlg, text="URL Server monitor:", anchor='w',
                     font=('Segoe UI', 12)).pack(fill='x', padx=20, pady=(20, 4))
        url_var = tk.StringVar(value=cfg.get('url', 'https://ut.rootsecurity.ro'))
        ctk.CTkEntry(dlg, textvariable=url_var, width=460, height=36,
                     placeholder_text="https://ut.rootsecurity.ro").pack(padx=20)

        ctk.CTkLabel(dlg, text="Cheie API:", anchor='w',
                     font=('Segoe UI', 12)).pack(fill='x', padx=20, pady=(12, 4))
        key_var = tk.StringVar(value=cfg.get('key', self._CLOUD_DEFAULTS['key']))
        ctk.CTkEntry(dlg, textvariable=key_var, width=460, height=36,
                     show='*', placeholder_text="pchm-...").pack(padx=20)

        status_lbl = ctk.CTkLabel(dlg, text='', font=('Segoe UI', 11))
        status_lbl.pack(pady=10)

        def test_conn():
            url = url_var.get().strip()
            key = key_var.get().strip()
            if not url or not key:
                status_lbl.configure(text='Completeaza URL si Cheie API', text_color='#FF5252')
                return
            status_lbl.configure(text='Se verifica...', text_color='gray')
            dlg.update()
            import urllib.request, ssl as _ssl
            try:
                ctx = _ssl.create_default_context()
                ctx.check_hostname = False
                ctx.verify_mode = _ssl.CERT_NONE
                req = urllib.request.Request(
                    url.rstrip('/') + '/api/v1/stats',
                    headers={'X-API-Key': key}, method='GET')
                with urllib.request.urlopen(req, timeout=8, context=ctx) as resp:
                    resp.read()
                status_lbl.configure(text='Conexiune reusita!', text_color='#00C853')
            except Exception as e:
                status_lbl.configure(text=f'Eroare: {str(e)[:55]}', text_color='#FF5252')

        def save():
            self._cloud_save_settings(url_var.get(), key_var.get())
            status_lbl.configure(text='Setari salvate!', text_color='#00C853')
            dlg.after(1200, dlg.destroy)

        btn_frame = ctk.CTkFrame(dlg, fg_color='transparent')
        btn_frame.pack(fill='x', padx=20, pady=4)
        ctk.CTkButton(btn_frame, text="Test conexiune", width=160,
                      fg_color='#1f2937', hover_color='#374151',
                      command=test_conn).pack(side='left', padx=4)
        ctk.CTkButton(btn_frame, text="Salveaza", width=120,
                      command=save).pack(side='right', padx=4)

    # ─── System tray ─────────────────────────────────────
    def _to_tray(self):
        if not TRAY_OK: return
        self.withdraw()
        img = Image.new('RGBA', (64, 64), (0,0,0,0))
        d   = ImageDraw.Draw(img)
        d.ellipse([4,4,60,60], fill='#00C853')
        d.rectangle([26,20,38,40], fill='white')
        d.rectangle([26,44,38,54], fill='white')

        def show(_): self.after(0, self.deiconify)
        def quit_(_): self.after(0, self._on_close)

        menu  = pystray.Menu(
            pystray.MenuItem("Deschide", show, default=True),
            pystray.MenuItem("Inchide", quit_)
        )
        self._tray = pystray.Icon("PC Health Monitor", img, "PC Health Monitor", menu)
        threading.Thread(target=self._tray.run, daemon=True).start()

    # ─── Close ───────────────────────────────────────────
    def _on_close(self):
        self._sampler.stop()
        if self._tick_id: self.after_cancel(self._tick_id)
        if self._ar_id:   self.after_cancel(self._ar_id)
        if TRAY_OK and hasattr(self, '_tray'):
            try: self._tray.stop()
            except Exception: pass
        self.destroy()


# ═══════════════════════════════════════════════════════
def main():
    import argparse
    parser = argparse.ArgumentParser(description=f'{TITLE} v{VERSION}')
    parser.add_argument('--headless', action='store_true',
                        help='Ruleaza fara interfata grafica si genereaza raport')
    parser.add_argument('--output', default='',
                        help='Fisier output (.json, .html sau .pdf). Fara argument: stdout JSON.')
    args, _ = parser.parse_known_args()

    if args.headless:
        col = HardwareCollector()
        print("Se colecteaza date hardware...", flush=True)
        data = col.collect()
        threading.Thread(target=_save_history, args=(data,), daemon=True).start()
        if args.output:
            ext = os.path.splitext(args.output)[1].lower()
            if ext == '.html':
                store = DataStore()
                HTMLExporter().export(data, store, args.output)
                print(f"Raport HTML salvat: {args.output}")
            elif ext == '.pdf':
                PDFExporter().export(data, args.output)
                print(f"Raport PDF salvat: {args.output}")
            else:
                with open(args.output, 'w', encoding='utf-8') as fh:
                    json.dump(data, fh, ensure_ascii=False, indent=2, default=str)
                print(f"Raport JSON salvat: {args.output}")
        else:
            print(json.dumps(data, ensure_ascii=False, indent=2, default=str))
        return

    app = App()
    app.mainloop()

if __name__ == '__main__':
    main()
