#!/usr/bin/env python3
"""
perf-tray — system-tray companion for perf.sh (TidalCycles performance modes).

Shows live thermals in the tray (a ParVagues-wave badge whose colour tracks heat)
and lets you switch perf.sh modes from a menu without leaving your livecoding
session.

Design notes
------------
* The perf/thermal LOGIC now lives in The Bridge (`tools/bridge/perf.py`) — the
  tray is just its Qt face (DRY; the web dashboard is the other face). Reading
  thermals is rootless; only *changing mode* needs root, via `sudo -n <SCRIPT>`
  against a ROOT-OWNED, sudoers-whitelisted copy of perf.sh (see
  perf-audio.sudoers). We never sudo the user-editable repo copy.
* hwmon device numbers are not stable across boots → chips resolved by name
  (in perf.py).
* PyQt5 because it ships in the Arch repos (python-pyqt5) — no extra deps.
"""

import math
import os
import subprocess
import sys
from pathlib import Path

from PyQt5.QtWidgets import (
    QApplication, QSystemTrayIcon, QMenu, QAction, QActionGroup,
)
from PyQt5.QtGui import QIcon, QPixmap, QPainter, QColor, QFont, QPen, QPainterPath
from PyQt5.QtCore import QTimer, Qt, QProcess, QRect

# Shared perf logic (DRY with the web Bridge). The tray lives at the repo root;
# perf.py lives under tools/bridge.
sys.path.insert(0, str(Path(__file__).resolve().parent / "tools" / "bridge"))
from perf import Thermals, detect_mode, MODES, SCRIPT  # noqa: E402
import launchers as LA  # noqa: E402

SERVICE = "perf-tray"   # systemd --user unit controlling autostart
REFRESH_MS = 2000
BRIDGE_URL = "http://127.0.0.1:8773/"


def autostart_enabled():
    """True if the perf-tray systemd --user service is enabled (starts at login)."""
    try:
        r = subprocess.run(
            ["systemctl", "--user", "is-enabled", SERVICE],
            capture_output=True, text=True, timeout=3,
        )
        return r.stdout.strip() == "enabled"
    except (OSError, subprocess.SubprocessError):
        return False


def set_autostart(on):
    """Enable/disable login autostart. Does not stop a running instance."""
    try:
        subprocess.run(
            ["systemctl", "--user", "enable" if on else "disable", SERVICE],
            capture_output=True, text=True, timeout=5,
        )
    except (OSError, subprocess.SubprocessError):
        pass


def temp_color(c):
    if c is None:
        return QColor("#607d8b")
    if c < 55:
        return QColor("#2e7d32")   # green
    if c < 70:
        return QColor("#f9a825")   # amber
    if c < 85:
        return QColor("#ef6c00")   # orange
    return QColor("#c62828")       # red


def make_icon(temp_c):
    """Heat-coloured badge with the ParVagues wave signature + the temp number."""
    px = QPixmap(64, 64)
    px.fill(Qt.transparent)
    p = QPainter(px)
    p.setRenderHint(QPainter.Antialiasing)

    # heat-coloured rounded badge
    p.setBrush(temp_color(temp_c))
    p.setPen(Qt.NoPen)
    p.drawRoundedRect(2, 2, 60, 60, 14, 14)

    # ParVagues signature wave across the lower third (two cycles, translucent)
    path = QPainterPath()
    mid, amp = 47, 6
    path.moveTo(2, mid)
    for x in range(2, 63):
        path.lineTo(x, mid - amp * math.sin((x - 2) / 60 * 2 * math.pi * 2))
    pen = QPen(QColor(255, 255, 255, 165))
    pen.setWidth(3)
    pen.setCapStyle(Qt.RoundCap)
    p.setPen(pen)
    p.drawPath(path)

    # temp number in the upper area, clear of the wave
    p.setPen(QColor("white"))
    f = QFont()
    f.setBold(True)
    f.setPixelSize(30 if (temp_c is not None and temp_c < 100) else 24)
    p.setFont(f)
    p.drawText(QRect(0, 0, 64, 44), Qt.AlignCenter, "--" if temp_c is None else str(temp_c))
    p.end()
    return QIcon(px)


class PerfTray:
    def __init__(self, app):
        self.app = app
        self.therm = Thermals()
        self.tray = QSystemTrayIcon(make_icon(self.therm.package_c()))
        self.tray.setToolTip("perf-tray")

        self.menu = QMenu()
        self._build_menu()
        self.tray.setContextMenu(self.menu)
        self.tray.show()

        self.timer = QTimer()
        self.timer.timeout.connect(self.refresh)
        self.timer.start(REFRESH_MS)
        self.refresh()

    def _build_menu(self):
        header = QAction("⚡ perf.sh modes", self.menu)
        header.setEnabled(False)
        self.menu.addAction(header)

        self.mode_group = QActionGroup(self.menu)
        self.mode_group.setExclusive(True)
        self.mode_actions = {}
        for key, (label, _flag) in MODES.items():
            act = QAction(label, self.menu, checkable=True)
            act.triggered.connect(lambda _c, k=key: self.set_mode(k))
            self.mode_group.addAction(act)
            self.menu.addAction(act)
            self.mode_actions[key] = act

        self.menu.addSeparator()
        bridge_act = QAction("⚓ Open Bridge", self.menu)
        bridge_act.triggered.connect(lambda: self._open_url(BRIDGE_URL))
        self.menu.addAction(bridge_act)

        launch_menu = self.menu.addMenu("Launch ▸")
        snap = LA.snapshot()
        for item in (*snap["web"], *snap["apps"]):
            act = QAction(item["name"], launch_menu)
            act.setEnabled(item["available"])
            act.triggered.connect(lambda _c, k=item["key"]: self._launch(k))
            launch_menu.addAction(act)

        self.menu.addSeparator()
        self.autostart_action = QAction("Start at login", self.menu, checkable=True)
        self.autostart_action.toggled.connect(self.on_autostart_toggled)
        self.menu.addAction(self.autostart_action)

        self.menu.addSeparator()
        # Live stats block — refreshed whenever the menu is about to show.
        self.stat_actions = []
        for _ in range(4):
            a = QAction("", self.menu)
            a.setEnabled(False)
            self.menu.addAction(a)
            self.stat_actions.append(a)
        self.menu.aboutToShow.connect(self.refresh)

        self.menu.addSeparator()
        quit_act = QAction("Quit perf-tray", self.menu)
        quit_act.triggered.connect(self.app.quit)
        self.menu.addAction(quit_act)

    def set_mode(self, key):
        flag = MODES[key][1]
        if not os.path.exists(SCRIPT):
            self.tray.showMessage(
                "perf-tray: not deployed",
                f"{SCRIPT} missing. Deploy it:\n"
                f"sudo install -m755 -o root -g root "
                f"~/Work/Sound/Tidal/perf.sh {SCRIPT}",
                QSystemTrayIcon.Warning, 8000,
            )
            return
        proc = QProcess(self.app)
        proc.setProgram("sudo")
        proc.setArguments(["-n", SCRIPT, flag])

        def done(_code, _status):
            err = bytes(proc.readAllStandardError()).decode(errors="replace")
            if proc.exitCode() != 0:
                self.tray.showMessage(
                    "perf-tray: mode change failed",
                    err.strip() or "sudo -n refused (check perf-audio.sudoers)",
                    QSystemTrayIcon.Critical, 8000,
                )
            QTimer.singleShot(400, self.refresh)

        proc.finished.connect(done)
        proc.start()

    def _open_url(self, url):
        QProcess.startDetached("xdg-open", [url])

    def _launch(self, key):
        r = LA.launch(key)
        if r["ok"] and r.get("url"):
            QTimer.singleShot(1300 if r["status"] == "launched" else 0,
                              lambda: self._open_url(r["url"]))
        if not r["ok"]:
            self.tray.showMessage("perf-tray", r["msg"], QSystemTrayIcon.Warning, 5000)

    def on_autostart_toggled(self, on):
        set_autostart(on)
        QTimer.singleShot(200, self.refresh)

    def refresh(self):
        t = self.therm
        pkg = t.package_c()
        self.tray.setIcon(make_icon(pkg))

        mode = detect_mode()
        if mode in self.mode_actions:
            self.mode_actions[mode].setChecked(True)

        # Reflect autostart state without re-triggering the toggle handler.
        self.autostart_action.blockSignals(True)
        self.autostart_action.setChecked(autostart_enabled())
        self.autostart_action.blockSignals(False)

        fmax, favg = t.freq_mhz()
        fans = t.fans_rpm()
        cores = t.cores_c()
        hottest = max(cores) if cores else pkg

        tip = (
            f"perf-tray — mode: {MODES.get(mode, ('?',))[0]}\n"
            f"package {pkg}°C  (hottest core {hottest}°C)\n"
            f"freq {favg}/{fmax} MHz avg/max\n"
            f"fans {' / '.join(str(f) for f in fans) or 'n/a'} RPM\n"
            f"throttle Δ {t.throttle_delta()} since launch"
        )
        self.tray.setToolTip(tip)

        rows = [
            f"mode: {MODES.get(mode, ('?',))[0]}",
            f"package {pkg}°C · hottest core {hottest}°C",
            f"freq {favg}/{fmax} MHz · fans {'/'.join(str(f) for f in fans) or 'n/a'} RPM",
            f"throttle Δ {t.throttle_delta()} since launch",
        ]
        for a, txt in zip(self.stat_actions, rows):
            a.setText(txt)


def main():
    app = QApplication([])
    app.setQuitOnLastWindowClosed(False)
    if not QSystemTrayIcon.isSystemTrayAvailable():
        print("No system tray available in this session.")
        return 1
    PerfTray(app)
    return app.exec_()


if __name__ == "__main__":
    raise SystemExit(main())
