#!/usr/bin/env python3
# SkillFishOS Monitor - standalone live sensor dashboard (temperature, frequency,
# GPU voltage, fan). Extracted from the Tuner's monitor; to grow with more panels.
# KDE-native PyQt6, brass/steampunk look. Bilingual (IT/EN). Sensors via skillfish-hud-val.
import sys, os, re, signal, subprocess, collections, csv, time, datetime
from PyQt6.QtCore import Qt, QThread, QTimer, pyqtSignal
from PyQt6.QtGui import (QIcon, QPainter, QColor, QPen, QBrush, QLinearGradient, QPainterPath, QFont)
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
                             QGridLayout, QLabel, QComboBox, QPushButton, QMessageBox)

ICON = "/usr/share/icons/hicolor/256x256/apps/skillfishos.png"

def _lang():
    v = (os.environ.get("LC_ALL") or os.environ.get("LC_MESSAGES")
         or os.environ.get("LANG") or os.environ.get("LANGUAGE") or "")
    return "it" if v.lower().startswith("it") else "en"
LANG = _lang()
def L(it, en): return it if LANG == "it" else en


class Chart(QWidget):
    """A live, custom-painted line chart panel (brass / steampunk look)."""
    def __init__(self, title, unit, series):
        super().__init__()
        self.title = title; self.unit = unit; self.series = series
        self.data = [collections.deque(maxlen=180) for _ in series]
        self.setMinimumSize(400, 190)

    def push(self, vals):
        for d, v in zip(self.data, vals):
            d.append(float(v) if v is not None else (d[-1] if d else 0.0))
        self.update()

    def paintEvent(self, _e):
        w, h = self.width(), self.height()
        if w <= 2 or h <= 2:
            return  # nothing sane to paint on a collapsed widget
        p = QPainter()
        # begin() can fail (e.g. device not ready); never paint on a dead device.
        if not p.begin(self):
            return
        try:
            p.setRenderHint(QPainter.RenderHint.Antialiasing)
            p.setPen(QPen(QColor(216, 168, 73, 80), 1)); p.setBrush(QColor(18, 15, 9, 240))
            p.drawRoundedRect(1, 1, w - 2, h - 2, 14, 14)
            gx0, gy0, gx1, gy1 = 14, 34, w - 14, h - 16
            gw, gh = gx1 - gx0, gy1 - gy0
            f = QFont("Inter"); f.setBold(True); f.setPointSize(10); p.setFont(f)
            p.setPen(QColor(216, 168, 73)); p.drawText(14, 22, self.title)
            allv = [v for d in self.data for v in d]
            if not allv:
                return
            lo, hi = min(allv), max(allv)
            if hi - lo < 1e-6: hi = lo + 1.0
            m = (hi - lo) * 0.14; lo -= m; hi += m
            span = (hi - lo) or 1.0
            p.setPen(QPen(QColor(216, 168, 73, 26), 1))
            for i in range(4):
                yy = int(gy0 + gh * i / 3); p.drawLine(gx0, yy, gx1, yy)
            fv = QFont("DejaVu Sans Mono"); fv.setPointSize(9); fv.setBold(True)
            lx = gx1
            for si, (label, col) in enumerate(self.series):
                d = self.data[si]; n = len(d); color = QColor(col)
                if n >= 2:
                    line = QPainterPath(); fill = QPainterPath()
                    for i, v in enumerate(d):
                        x = gx0 + gw * (i / (n - 1)); y = gy1 - gh * ((v - lo) / span)
                        if i == 0: line.moveTo(x, y); fill.moveTo(x, gy1); fill.lineTo(x, y)
                        else: line.lineTo(x, y); fill.lineTo(x, y)
                    fill.lineTo(gx1, gy1); fill.closeSubpath()
                    grad = QLinearGradient(0, gy0, 0, gy1)
                    c0 = QColor(color); c0.setAlpha(75); c1 = QColor(color); c1.setAlpha(0)
                    grad.setColorAt(0, c0); grad.setColorAt(1, c1)
                    p.setPen(Qt.PenStyle.NoPen); p.setBrush(QBrush(grad)); p.drawPath(fill)
                    glow = QColor(color); glow.setAlpha(55)
                    p.setBrush(Qt.BrushStyle.NoBrush); p.setPen(QPen(glow, 5)); p.drawPath(line)
                    p.setPen(QPen(color, 2)); p.drawPath(line)
                cur = d[-1] if n else 0.0
                txt = "%s %s" % (label, ("%d" % round(cur)) if abs(cur) >= 10 else ("%.2f" % cur))
                p.setFont(fv); p.setPen(color)
                tw = p.fontMetrics().horizontalAdvance(txt)
                lx -= tw + 14; p.drawText(int(lx), 22, txt)
        except Exception:
            pass  # never let a paint glitch tear down the painter mid-flight
        finally:
            if p.isActive():
                p.end()


def rd(key):
    try:
        out = subprocess.run(["skillfish-hud-val", key], capture_output=True, text=True, timeout=0.6).stdout
        m = re.search(r'-?\d+(?:\.\d+)?', out)
        return float(m.group()) if m else None
    except Exception:
        return None


class Sampler(QThread):
    """Reads sensors off the GUI thread so the UI never blocks on subprocess calls."""
    sample = pyqtSignal(dict)
    KEYS = ("cpu_temp", "gpu_temp", "cpu_mhz", "gpu_freq", "gpu_mv", "fan")

    def __init__(self):
        super().__init__()
        self._interval = 0.7
        self._stop = False

    def setIntervalMs(self, ms):
        try:
            self._interval = max(0.1, float(ms) / 1000.0)
        except Exception:
            self._interval = 0.7

    def stop(self):
        self._stop = True

    def run(self):
        while not self._stop:
            vals = {k: rd(k) for k in self.KEYS}
            if self._stop:
                break
            self.sample.emit(vals)
            t = 0.0
            while t < self._interval and not self._stop:
                self.msleep(50); t += 0.05


class Monitor(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle(L("SkillFishOS — Monitor", "SkillFishOS — Monitor"))
        self.setWindowIcon(QIcon(ICON))
        self.resize(920, 600)
        w = QWidget(); self.setCentralWidget(w)
        w.setStyleSheet("QWidget{background:#0d0b06;}")
        v = QVBoxLayout(w); v.setContentsMargins(14, 12, 14, 12); v.setSpacing(10)

        top = QHBoxLayout()
        hdr = QLabel(L("◉  MONITOR LIVE", "◉  LIVE MONITOR"))
        hdr.setStyleSheet("color:#d8a849;font-weight:bold;font-size:14px;letter-spacing:3px;")
        top.addWidget(hdr); top.addStretch(1)
        # benchmark recorder: telemetry to CSV + summary stats
        self._rec = None  # (csv.writer, file handle, rows, t0)
        self.rec_btn = QPushButton(L("● REC", "● REC")); self.rec_btn.setCheckable(True)
        self.rec_btn.setToolTip(L("Registra la telemetria su CSV (per benchmark/sessioni di gioco)",
                                  "Record telemetry to CSV (for benchmarks/gaming sessions)"))
        self.rec_btn.setStyleSheet(
            "QPushButton{color:#b9a07a;background:rgba(216,168,73,0.10);border:1px solid rgba(216,168,73,0.3);"
            "border-radius:8px;padding:4px 12px;font-weight:bold;}"
            "QPushButton:checked{color:#fff;background:#a03020;border-color:#e05540;}")
        self.rec_btn.toggled.connect(self._rec_toggle)
        top.addWidget(self.rec_btn)
        top.addWidget(QLabel(L("Aggiornamento:", "Refresh:")))
        self.iv = QComboBox()
        for ms, lab in [(300, "0.3 s"), (700, "0.7 s"), (1000, "1 s"), (2000, "2 s")]:
            self.iv.addItem(lab, ms)
        self.iv.setCurrentIndex(1)
        self.iv.currentIndexChanged.connect(lambda: self.sampler.setIntervalMs(self.iv.currentData()))
        top.addWidget(self.iv)
        v.addLayout(top)

        grid = QGridLayout(); grid.setSpacing(12)
        self.ch_temp = Chart(L("Temperatura (°C)", "Temperature (°C)"), "°C", [("CPU", "#e8c878"), ("GPU", "#e07b39")])
        self.ch_freq = Chart(L("Frequenza (MHz)", "Frequency (MHz)"), "MHz", [("CPU", "#5fd24f"), ("GPU", "#49b6e0")])
        self.ch_volt = Chart(L("Voltaggio GPU (mV)", "GPU voltage (mV)"), "mV", [("GPU", "#c98be0")])
        self.ch_fan = Chart(L("Ventola (RPM)", "Fan (RPM)"), "RPM", [("FAN", "#d8a849")])
        grid.addWidget(self.ch_temp, 0, 0); grid.addWidget(self.ch_freq, 0, 1)
        grid.addWidget(self.ch_volt, 1, 0); grid.addWidget(self.ch_fan, 1, 1)
        v.addLayout(grid)

        self.sampler = Sampler()
        self.sampler.setIntervalMs(self.iv.currentData())
        self.sampler.sample.connect(self._apply)  # queued: runs on GUI thread
        self.sampler.start()

    def _apply(self, vals):
        self.ch_temp.push([vals.get("cpu_temp"), vals.get("gpu_temp")])
        self.ch_freq.push([vals.get("cpu_mhz"), vals.get("gpu_freq")])
        self.ch_volt.push([vals.get("gpu_mv")])
        self.ch_fan.push([vals.get("fan")])
        if self._rec:
            w, fh, rows, t0 = self._rec
            row = [round(time.time() - t0, 2)] + [vals.get(k) for k in self.REC_KEYS]
            w.writerow(row); rows.append(row)

    # ---------- benchmark recorder ----------
    REC_KEYS = ["cpu_temp", "gpu_temp", "cpu_mhz", "gpu_freq", "gpu_mv", "fan"]

    def _rec_toggle(self, on):
        if on:
            d = os.path.expanduser("~/SkillFishOS-benchmarks")
            os.makedirs(d, exist_ok=True)
            path = os.path.join(d, datetime.datetime.now().strftime("rec-%Y%m%d-%H%M%S.csv"))
            fh = open(path, "w", newline="")
            w = csv.writer(fh); w.writerow(["t_sec"] + self.REC_KEYS)
            self._rec = (w, fh, [], time.time())
            self._rec_path = path
            self.rec_btn.setText(L("■ STOP", "■ STOP"))
        else:
            rec, self._rec = self._rec, None
            self.rec_btn.setText(L("● REC", "● REC"))
            if not rec:
                return
            _w, fh, rows, t0 = rec
            fh.close()
            if not rows:
                return
            dur = rows[-1][0]
            lines = [L("Registrazione: %.0f s, %d campioni" % (dur, len(rows)),
                       "Recording: %.0f s, %d samples" % (dur, len(rows))), ""]
            names = {"cpu_temp": "CPU °C", "gpu_temp": "GPU °C", "cpu_mhz": "CPU MHz",
                     "gpu_freq": "GPU MHz", "gpu_mv": "GPU mV", "fan": "Fan RPM"}
            for i, k in enumerate(self.REC_KEYS, start=1):
                series = [r[i] for r in rows if isinstance(r[i], (int, float))]
                if series:
                    lines.append("%-8s  min %.0f · med %.0f · max %.0f"
                                 % (names[k], min(series), sum(series) / len(series), max(series)))
            lines += ["", L("CSV salvato in:", "CSV saved to:"), self._rec_path]
            QMessageBox.information(self, L("Riepilogo registrazione", "Recording summary"), "\n".join(lines))

    def closeEvent(self, e):
        if self._rec:
            try: self._rec[1].close()
            except Exception: pass  # best-effort flush of an in-progress recording
            self._rec = None
        try:
            self.sampler.stop()
            self.sampler.wait(1500)
        except Exception:
            pass  # best-effort: sampler may already be stopped
        super().closeEvent(e)


def main():
    app = QApplication(sys.argv)
    app.setApplicationName("SkillFishOS Monitor")
    app.setDesktopFileName("os.skillfish.monitor")
    app.setWindowIcon(QIcon(ICON))
    win = Monitor(); win.show()
    # Always stop the sampler thread before the event loop tears widgets down,
    # so we never destroy a chart while it (or a paint) is mid-flight.
    app.aboutToQuit.connect(win.sampler.stop)
    # Turn SIGTERM/SIGINT (logout, kill) into a graceful quit instead of a kill
    # mid-paint. The keepalive timer lets the Python signal handlers actually run.
    signal.signal(signal.SIGTERM, lambda *_a: app.quit())
    signal.signal(signal.SIGINT, lambda *_a: app.quit())
    _keepalive = QTimer(); _keepalive.start(250); _keepalive.timeout.connect(lambda: None)
    win._keepalive = _keepalive  # keep a reference alive
    sq = os.environ.get("SFX_SELFQUIT_MS")  # self-test hook: graceful close after N ms
    if sq:
        QTimer.singleShot(int(sq), win.close)
    sys.exit(app.exec())

if __name__ == "__main__":
    main()
