pragma Singleton
import Qt.labs.folderlistmodel

import QtQuick
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services.UI

Singleton {
  id: root

  // Component registration - only poll when something needs system stat data
  function registerComponent(componentId) {
    root._registered[componentId] = true;
    root._registered = Object.assign({}, root._registered);
    Logger.d("SystemStat", "Component registered:", componentId, "- total:", root._registeredCount);
  }

  function unregisterComponent(componentId) {
    delete root._registered[componentId];
    root._registered = Object.assign({}, root._registered);
    Logger.d("SystemStat", "Component unregistered:", componentId, "- total:", root._registeredCount);
  }

  property var _registered: ({})
  readonly property int _registeredCount: Object.keys(_registered).length
  readonly property bool _lockScreenActive: PanelService.lockScreen?.active ?? false
  readonly property bool shouldRun: _registeredCount > 0 && !_lockScreenActive

  // Polling intervals (hardcoded to sensible values per stat type)
  readonly property int cpuUsageIntervalMs: 1000
  readonly property int cpuFreqIntervalMs: 3000
  readonly property int memIntervalMs: 5000
  readonly property int networkIntervalMs: 3000
  readonly property int loadAvgIntervalMs: 10000
  readonly property int diskIntervalMs: 30000
  readonly property int gpuIntervalMs: 5000

  // Public values
  property real cpuUsage: 0
  property list<real> coresUsage: []
  property real cpuTemp: 0
  property string cpuFreq: "0.0GHz"
  property real cpuFreqRatio: 0
  property real cpuGlobalMaxFreq: 3.5
  property real gpuTemp: 0
  property bool gpuAvailable: false
  property string gpuType: "" // "amd", "intel", "nvidia"
  property real memGb: 0
  property real memPercent: 0
  property real memTotalGb: 0
  property real swapGb: 0
  property real swapPercent: 0
  property real swapTotalGb: 0
  property var diskPercents: ({})
  property var diskAvailPercents: ({}) // available disk space in percent
  property var diskUsedGb: ({}) // Used space in GB per mount point
  property var diskAvailableGb: ({}) // available space in GB per mount point
  property var diskSizeGb: ({}) // Total size in GB per mount point
  property real rxSpeed: 0
  property real txSpeed: 0
  property real zfsArcSizeKb: 0 // ZFS ARC cache size in KB
  property real zfsArcCminKb: 0 // ZFS ARC minimum (non-reclaimable) size in KB
  property real loadAvg1: 0
  property real loadAvg5: 0
  property real loadAvg15: 0
  property int nproc: 0 // Number of cpu cores

  // History arrays (2 minutes of data, length computed from polling interval)
  // Pre-filled with zeros so the graph scrolls smoothly from the start
  readonly property int historyDurationMs: (1 * 60 * 1000) // 1 minute

  // Computed history lengths based on polling intervals
  readonly property int cpuHistoryLength: Math.ceil(historyDurationMs / cpuUsageIntervalMs)
  readonly property int gpuHistoryLength: Math.ceil(historyDurationMs / gpuIntervalMs)
  readonly property int memHistoryLength: Math.ceil(historyDurationMs / memIntervalMs)
  readonly property int diskHistoryLength: Math.max(10, Math.ceil(historyDurationMs / diskIntervalMs))
  readonly property int networkHistoryLength: Math.ceil(historyDurationMs / networkIntervalMs)

  property var cpuHistory: new Array(cpuHistoryLength).fill(0)
  property var cpuTempHistory: new Array(cpuHistoryLength).fill(40)  // Reasonable default temp
  property var gpuTempHistory: new Array(gpuHistoryLength).fill(40)  // Reasonable default temp
  property var memHistory: new Array(memHistoryLength).fill(0)
  property var diskHistories: ({}) // Keyed by mount path, initialized on first update
  property var rxSpeedHistory: new Array(networkHistoryLength).fill(0)
  property var txSpeedHistory: new Array(networkHistoryLength).fill(0)

  // Historical min/max tracking (since shell started) for consistent graph scaling
  // Temperature defaults create a valid 30-80°C range that expands as real data comes in
  property real cpuTempHistoryMin: 30
  property real cpuTempHistoryMax: 80
  property real gpuTempHistoryMin: 30
  property real gpuTempHistoryMax: 80
  // Network uses autoscaling from current history window
  // Disk is always 0-100%

  // History management - called from update functions, not change handlers
  // (change handlers don't fire when value stays the same)
  function pushCpuHistory() {
    let h = cpuHistory.slice();
    h.push(cpuUsage);
    if (h.length > cpuHistoryLength)
      h.shift();
    cpuHistory = h;
  }

  function pushCpuTempHistory() {
    if (cpuTemp > 0) {
      if (cpuTemp < cpuTempHistoryMin)
        cpuTempHistoryMin = cpuTemp;
      if (cpuTemp > cpuTempHistoryMax)
        cpuTempHistoryMax = cpuTemp;
    }
    let h = cpuTempHistory.slice();
    h.push(cpuTemp);
    if (h.length > cpuHistoryLength)
      h.shift();
    cpuTempHistory = h;
  }

  function pushGpuHistory() {
    if (gpuTemp > 0) {
      if (gpuTemp < gpuTempHistoryMin)
        gpuTempHistoryMin = gpuTemp;
      if (gpuTemp > gpuTempHistoryMax)
        gpuTempHistoryMax = gpuTemp;
    }
    let h = gpuTempHistory.slice();
    h.push(gpuTemp);
    if (h.length > gpuHistoryLength)
      h.shift();
    gpuTempHistory = h;
  }

  function pushMemHistory() {
    let h = memHistory.slice();
    h.push(memPercent);
    if (h.length > memHistoryLength)
      h.shift();
    memHistory = h;
  }

  function pushDiskHistory() {
    let newHistories = {};
    for (let path in diskPercents) {
      // Pre-fill with zeros if this is a new path
      let h = diskHistories[path] ? diskHistories[path].slice() : new Array(diskHistoryLength).fill(0);
      h.push(diskPercents[path]);
      if (h.length > diskHistoryLength)
        h.shift();
      newHistories[path] = h;
    }
    diskHistories = newHistories;
  }

  function pushNetworkHistory() {
    let rxH = rxSpeedHistory.slice();
    rxH.push(rxSpeed);
    if (rxH.length > networkHistoryLength)
      rxH.shift();
    rxSpeedHistory = rxH;

    let txH = txSpeedHistory.slice();
    txH.push(txSpeed);
    if (txH.length > networkHistoryLength)
      txH.shift();
    txSpeedHistory = txH;
  }

  // Network max speed tracking (autoscales from current history window)
  // Minimum floor of 1 MB/s so graph doesn't fluctuate at low speeds
  readonly property real rxMaxSpeed: {
    const max = Math.max(...rxSpeedHistory);
    return Math.max(max, 1000000); // 1 MB/s floor
  }
  readonly property real txMaxSpeed: {
    const max = Math.max(...txSpeedHistory);
    return Math.max(max, 512000); // 512 KB/s floor
  }

  // Ready-to-use ratios based on current maximums (0..1 range)
  readonly property real rxRatio: rxMaxSpeed > 0 ? Math.min(1, rxSpeed / rxMaxSpeed) : 0
  readonly property real txRatio: txMaxSpeed > 0 ? Math.min(1, txSpeed / txMaxSpeed) : 0

  // Color resolution (respects useCustomColors setting)
  readonly property color warningColor: Settings.data.systemMonitor.useCustomColors ? (Settings.data.systemMonitor.warningColor || Color.mTertiary) : Color.mTertiary
  readonly property color criticalColor: Settings.data.systemMonitor.useCustomColors ? (Settings.data.systemMonitor.criticalColor || Color.mError) : Color.mError

  // Threshold values from settings
  readonly property int cpuWarningThreshold: Settings.data.systemMonitor.cpuWarningThreshold
  readonly property int cpuCriticalThreshold: Settings.data.systemMonitor.cpuCriticalThreshold
  readonly property int tempWarningThreshold: Settings.data.systemMonitor.tempWarningThreshold
  readonly property int tempCriticalThreshold: Settings.data.systemMonitor.tempCriticalThreshold
  readonly property int gpuWarningThreshold: Settings.data.systemMonitor.gpuWarningThreshold
  readonly property int gpuCriticalThreshold: Settings.data.systemMonitor.gpuCriticalThreshold
  readonly property int memWarningThreshold: Settings.data.systemMonitor.memWarningThreshold
  readonly property int memCriticalThreshold: Settings.data.systemMonitor.memCriticalThreshold
  readonly property int swapWarningThreshold: Settings.data.systemMonitor.swapWarningThreshold
  readonly property int swapCriticalThreshold: Settings.data.systemMonitor.swapCriticalThreshold
  readonly property int diskWarningThreshold: Settings.data.systemMonitor.diskWarningThreshold
  readonly property int diskCriticalThreshold: Settings.data.systemMonitor.diskCriticalThreshold
  readonly property int diskAvailWarningThreshold: Settings.data.systemMonitor.diskAvailWarningThreshold
  readonly property int diskAvailCriticalThreshold: Settings.data.systemMonitor.diskAvailCriticalThreshold

  // Computed warning/critical states (uses >= inclusive comparison)
  readonly property bool cpuWarning: cpuUsage >= cpuWarningThreshold
  readonly property bool cpuCritical: cpuUsage >= cpuCriticalThreshold
  readonly property bool tempWarning: cpuTemp >= tempWarningThreshold
  readonly property bool tempCritical: cpuTemp >= tempCriticalThreshold
  readonly property bool gpuWarning: gpuAvailable && gpuTemp >= gpuWarningThreshold
  readonly property bool gpuCritical: gpuAvailable && gpuTemp >= gpuCriticalThreshold
  readonly property bool memWarning: memPercent >= memWarningThreshold
  readonly property bool memCritical: memPercent >= memCriticalThreshold
  readonly property bool swapWarning: swapPercent >= swapWarningThreshold
  readonly property bool swapCritical: swapPercent >= swapCriticalThreshold

  // Helper functions for disk (disk path is dynamic)
  function isDiskWarning(diskPath, available = false) {
    return available ? (diskAvailPercents[diskPath] || 0) <= diskAvailWarningThreshold : (diskPercents[diskPath] || 0) >= diskWarningThreshold;
  }

  function isDiskCritical(diskPath, available = false) {
    return available ? (diskAvailPercents[diskPath] || 0) <= diskAvailCriticalThreshold : (diskPercents[diskPath] || 0) >= diskCriticalThreshold;
  }

  // Ready-to-use stat colors (for gauges, panels, icons)
  readonly property color cpuColor: cpuCritical ? criticalColor : (cpuWarning ? warningColor : Color.mPrimary)
  readonly property color tempColor: tempCritical ? criticalColor : (tempWarning ? warningColor : Color.mPrimary)
  readonly property color gpuColor: gpuCritical ? criticalColor : (gpuWarning ? warningColor : Color.mPrimary)
  readonly property color memColor: memCritical ? criticalColor : (memWarning ? warningColor : Color.mPrimary)
  readonly property color swapColor: swapCritical ? criticalColor : (swapWarning ? warningColor : Color.mPrimary)

  function getCoreUsageColor(usage) {
    if (usage >= cpuCriticalThreshold)
      return criticalColor;
    if (usage >= cpuWarningThreshold)
      return warningColor;
    return Color.mPrimary;
  }

  function getDiskColor(diskPath, available = false) {
    return isDiskCritical(diskPath, available) ? criticalColor : (isDiskWarning(diskPath, available) ? warningColor : Color.mPrimary);
  }

  // Internal state for CPU calculation
  property var prevCpuStats: null
  property var prevCpuCoresStats: null

  // Internal state for network speed calculation
  // Previous Bytes need to be stored as 'real' as they represent the total of bytes transfered
  // since the computer started, so their value will easily overlfow a 32bit int.
  property real prevRxBytes: 0
  property real prevTxBytes: 0
  property real prevTime: 0

  // Cpu temperature is the most complex
  readonly property var supportedTempCpuSensorNames: ["coretemp", "k10temp", "zenpower"]
  property string cpuTempSensorName: ""
  property string cpuTempHwmonPath: ""
  // For Intel coretemp averaging of all cores/sensors
  property var intelTempValues: []
  property int intelTempFilesChecked: 0
  property int intelTempMaxFiles: 20 // Will test up to temp20_input

  // Thermal zone fallback (for ARM SoCs with SCMI sensors, etc.)
  property var cpuThermalZonePaths: [] // All matching CPU zones for averaging
  property string gpuThermalZonePath: ""

  // GPU temperature detection
  // On dual-GPU systems, we prioritize discrete GPUs over integrated GPUs
  // Priority: NVIDIA (opt-in) > AMD dGPU > Intel Arc > AMD iGPU
  // Note: NVIDIA requires opt-in because nvidia-smi wakes the dGPU on laptops, draining battery
  readonly property var supportedTempGpuSensorNames: ["amdgpu", "xe"]
  property string gpuTempHwmonPath: ""
  property var foundGpuSensors: [] // [{hwmonPath, type, hasDedicatedVram}]
  property int gpuVramCheckIndex: 0

  // --------------------------------------------
  Component.onCompleted: {
    Logger.i("SystemStat", "Service started (polling deferred until a consumer registers).");

    // Kickoff the cpu name detection for temperature (one-time probes, not polling)
    cpuTempNameReader.checkNext();

    // Kickoff the gpu sensor detection for temperature (one-time probes, not polling)
    gpuTempNameReader.checkNext();

    // Get nproc on startup (one-time)
    nprocProcess.running = true;
  }

  onShouldRunChanged: {
    if (shouldRun) {
      // Reset differential state so first readings after resume are clean
      root.prevCpuStats = null;
      root.prevCpuCoresStats = null;
      root.prevTime = 0;

      // Trigger initial reads
      zfsArcStatsFile.reload();
      loadAvgFile.reload();

      // Start persistent disk shell
      if (!dfShell.running) {
        dfShell.running = true;
      }
    } else {
      // Stop persistent disk shell
      if (dfShell.running) {
        dfShell.running = false;
      }
    }
  }

  // Re-run GPU detection when dGPU opt-in setting changes
  Connections {
    target: Settings.data.systemMonitor
    function onEnableDgpuMonitoringChanged() {
      Logger.i("SystemStat", "dGPU monitoring opt-in setting changed, re-detecting GPUs");
      restartGpuDetection();
    }
  }

  // Reset differential state after suspend so the first reading is treated as fresh
  Connections {
    target: Time
    function onResumed() {
      Logger.i("SystemStat", "System resumed - resetting differential state");
      root.prevCpuStats = null;
      root.prevCpuCoresStats = null;
      root.prevTime = 0;
    }
  }

  function restartGpuDetection() {
    // Reset GPU state
    root.gpuAvailable = false;
    root.gpuType = "";
    root.gpuTempHwmonPath = "";
    root.gpuTemp = 0;
    root.foundGpuSensors = [];
    root.gpuVramCheckIndex = 0;

    // Restart GPU detection
    gpuTempNameReader.currentIndex = 0;
    gpuTempNameReader.checkNext();
  }

  // --------------------------------------------
  // Timer for CPU usage and temperature
  Timer {
    id: cpuTimer
    interval: root.cpuUsageIntervalMs
    repeat: true
    running: root.shouldRun
    triggeredOnStart: true
    onTriggered: {
      cpuStatFile.reload();
      updateCpuTemperature();
    }
  }

  // Timer for CPU frequency (slower — /proc/cpuinfo is large and freq changes infrequently)
  Timer {
    id: cpuFreqTimer
    interval: root.cpuFreqIntervalMs
    repeat: true
    running: root.shouldRun
    triggeredOnStart: true
    onTriggered: cpuInfoFile.reload()
  }

  // Timer for load average
  Timer {
    id: loadAvgTimer
    interval: root.loadAvgIntervalMs
    repeat: true
    running: root.shouldRun
    triggeredOnStart: true
    onTriggered: loadAvgFile.reload()
  }

  // Timer for memory stats
  Timer {
    id: memoryTimer
    interval: root.memIntervalMs
    repeat: true
    running: root.shouldRun
    triggeredOnStart: true
    onTriggered: {
      memInfoFile.reload();
      zfsArcStatsFile.reload();
    }
  }

  // Timer for disk usage
  Timer {
    id: diskTimer
    interval: root.diskIntervalMs
    repeat: true
    running: root.shouldRun
    triggeredOnStart: true
    onTriggered: {
      if (dfShell.running) {
        dfShell.write("df --output=target,pcent,used,size,avail --block-size=1 -x efivarfs 2>/dev/null; echo '@@DF_END@@'\n");
      }
    }
  }

  // Timer for network speeds
  Timer {
    id: networkTimer
    interval: root.networkIntervalMs
    repeat: true
    running: root.shouldRun
    triggeredOnStart: true
    onTriggered: netDevFile.reload()
  }

  // Timer for GPU temperature
  Timer {
    id: gpuTempTimer
    interval: root.gpuIntervalMs
    repeat: true
    running: root.shouldRun && root.gpuAvailable
    triggeredOnStart: true
    onTriggered: updateGpuTemperature()
  }

  // --------------------------------------------
  // FileView components for reading system files
  FileView {
    id: memInfoFile
    path: "/proc/meminfo"
    onLoaded: parseMemoryInfo(text())
  }

  FileView {
    id: cpuStatFile
    path: "/proc/stat"
    onLoaded: calculateCpuUsage(text())
  }

  FileView {
    id: netDevFile
    path: "/proc/net/dev"
    onLoaded: calculateNetworkSpeed(text())
  }

  FileView {
    id: loadAvgFile
    path: "/proc/loadavg"
    onLoaded: parseLoadAverage(text())
  }

  // ZFS ARC stats file (only exists on ZFS systems)
  FileView {
    id: zfsArcStatsFile
    path: "/proc/spl/kstat/zfs/arcstats"
    printErrors: false
    onLoaded: parseZfsArcStats(text())
    onLoadFailed: {
      // File doesn't exist (non-ZFS system), set ARC values to 0
      root.zfsArcSizeKb = 0;
      root.zfsArcCminKb = 0;
    }
  }

  // --------------------------------------------
  // Persistent shell for disk usage queries (avoids fork+exec of large Quickshell process every poll)
  // Uses 'df' aka 'disk free'
  // "-x efivarfs" skips efivarfs mountpoints, for which the `statfs` syscall may cause system-wide stuttering
  // --block-size=1 gives us bytes for precise GB calculation
  // Timer writes commands to stdin; SplitParser reads output delimited by @@DF_END@@
  Process {
    id: dfShell
    command: ["sh"]
    stdinEnabled: true
    running: false

    onRunningChanged: {
      if (!running && root.shouldRun) {
        // Restart if it died unexpectedly while we still need it
        Logger.w("SystemStat", "Disk shell exited unexpectedly, restarting");
        Qt.callLater(() => {
                       dfShell.running = true;
                     });
      }
    }

    stdout: SplitParser {
      splitMarker: "@@DF_END@@"
      onRead: data => {
        const lines = data.trim().split('\n');
        const newPercents = {};
        const newAvailPercents = {};
        const newUsedGb = {};
        const newSizeGb = {};
        const newAvailableGb = {};
        const bytesPerGb = 1000 * 1000 * 1000;
        // Start from line 1 (skip header)
        for (var i = 1; i < lines.length; i++) {
          const parts = lines[i].trim().split(/\s+/);
          if (parts.length >= 5) {
            const target = parts[0];
            const percent = parseInt(parts[1].replace(/[^0-9]/g, '')) || 0;
            const usedBytes = parseFloat(parts[2]) || 0;
            const sizeBytes = parseFloat(parts[3]) || 0;
            const availBytes = parseFloat(parts[4]) || 0;
            const availPercent = sizeBytes > 0 ? (availBytes / sizeBytes) * 100 : 0;
            newPercents[target] = percent;
            newAvailPercents[target] = Math.round(availPercent);
            newUsedGb[target] = usedBytes / bytesPerGb;
            newSizeGb[target] = sizeBytes / bytesPerGb;
            newAvailableGb[target] = availBytes / bytesPerGb;
          }
        }
        root.diskPercents = newPercents;
        root.diskAvailPercents = newAvailPercents;
        root.diskUsedGb = newUsedGb;
        root.diskSizeGb = newSizeGb;
        root.diskAvailableGb = newAvailableGb;
        root.pushDiskHistory();
      }
    }
  }

  // Process to get number of processors
  Process {
    id: nprocProcess
    command: ["nproc"]
    running: false
    stdout: StdioCollector {
      onStreamFinished: {
        root.nproc = parseInt(text.trim());
      }
    }
  }

  // FileView to get avg cpu frequency (replaces subprocess spawn of `cat /proc/cpuinfo`)
  FileView {
    id: cpuInfoFile
    path: "/proc/cpuinfo"
    onLoaded: {
      let txt = text();
      let matches = txt.match(/cpu MHz\s+:\s+([0-9.]+)/g);
      if (matches && matches.length > 0) {
        let totalFreq = 0.0;
        for (let i = 0; i < matches.length; i++) {
          totalFreq += parseFloat(matches[i].split(":")[1]);
        }
        let avgFreq = (totalFreq / matches.length) / 1000.0;
        root.cpuFreq = avgFreq.toFixed(1) + "GHz";
        cpuMaxFreqFile.reload();
        if (avgFreq > root.cpuGlobalMaxFreq)
        root.cpuGlobalMaxFreq = avgFreq;
        if (root.cpuGlobalMaxFreq > 0) {
          root.cpuFreqRatio = Math.min(1.0, avgFreq / root.cpuGlobalMaxFreq);
        }
      }
    }
  }

  // FileView to get maximum CPU frequency limit (replaces subprocess spawn)
  // Reads cpu0's scaling_max_freq as representative value
  FileView {
    id: cpuMaxFreqFile
    path: "/sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq"
    printErrors: false
    onLoaded: {
      let maxKHz = parseInt(text().trim());
      if (!isNaN(maxKHz) && maxKHz > 0) {
        let newMaxFreq = maxKHz / 1000000.0;
        if (Math.abs(root.cpuGlobalMaxFreq - newMaxFreq) > 0.01) {
          root.cpuGlobalMaxFreq = newMaxFreq;
        }
      }
    }
  }

  // --------------------------------------------
  // --------------------------------------------
  // CPU Temperature
  // It's more complex.
  // ----
  // #1 - Find a common cpu sensor name ie: "coretemp", "k10temp", "zenpower"
  FileView {
    id: cpuTempNameReader
    property int currentIndex: 0
    printErrors: false

    function checkNext() {
      if (currentIndex >= 16) {
        // No hwmon sensor found, try thermal_zone fallback (ARM SoCs, SCMI, etc.)
        thermalZoneScanner.startScan();
        return;
      }

      //Logger.i("SystemStat", "---- Probing: hwmon", currentIndex)
      cpuTempNameReader.path = `/sys/class/hwmon/hwmon${currentIndex}/name`;
      cpuTempNameReader.reload();
    }

    onLoaded: {
      const name = text().trim();
      if (root.supportedTempCpuSensorNames.includes(name)) {
        root.cpuTempSensorName = name;
        root.cpuTempHwmonPath = `/sys/class/hwmon/hwmon${currentIndex}`;
        Logger.i("SystemStat", `Found ${root.cpuTempSensorName} CPU thermal sensor at ${root.cpuTempHwmonPath}`);
      } else {
        currentIndex++;
        Qt.callLater(() => {
                       // Qt.callLater is mandatory
                       checkNext();
                     });
      }
    }

    onLoadFailed: function (error) {
      currentIndex++;
      Qt.callLater(() => {
                     // Qt.callLater is mandatory
                     checkNext();
                   });
    }
  }

  // ----
  // #2 - Read sensor value
  FileView {
    id: cpuTempReader
    printErrors: false

    onLoaded: {
      const data = text().trim();
      if (root.cpuTempSensorName === "coretemp") {
        // For Intel, collect all temperature values
        const temp = parseInt(data) / 1000.0;
        //console.log(temp, cpuTempReader.path)
        root.intelTempValues.push(temp);
        Qt.callLater(() => {
                       // Qt.callLater is mandatory
                       checkNextIntelTemp();
                     });
      } else {
        // For AMD sensors (k10temp and zenpower), directly set the temperature
        root.cpuTemp = Math.round(parseInt(data) / 1000.0);
        root.pushCpuTempHistory();
      }
    }
    onLoadFailed: function (error) {
      Qt.callLater(() => {
                     // Qt.callLater is mandatory
                     checkNextIntelTemp();
                   });
    }
  }

  // --------------------------------------------
  // Thermal zone fallback for CPU and GPU temperature
  // Used on ARM SoCs (e.g., SCMI sensors) where hwmon doesn't expose
  // coretemp/k10temp/zenpower. Scans /sys/class/thermal/thermal_zoneN/type
  // for CPU and GPU zone names, then reads temp from all matching zones.
  //
  // CPU: reads all cpu-*-thermal zones and reports the hottest core.
  // GPU: prefers gpu-avg-thermal (firmware average), falls back to max of gpu[0-9]-thermal.

  FileView {
    id: thermalZoneScanner
    property int currentIndex: 0
    property var cpuZones: []
    property var gpuZones: []
    property string gpuAvgZonePath: ""
    printErrors: false

    function startScan() {
      currentIndex = 0;
      cpuZones = [];
      gpuZones = [];
      gpuAvgZonePath = "";
      checkNext();
    }

    function checkNext() {
      if (currentIndex >= 20) {
        finishScan();
        return;
      }
      thermalZoneScanner.path = `/sys/class/thermal/thermal_zone${currentIndex}/type`;
      thermalZoneScanner.reload();
    }

    onLoaded: {
      const name = text().trim();
      const zonePath = `/sys/class/thermal/thermal_zone${currentIndex}`;
      if (name.startsWith("cpu") && name.endsWith("thermal")) {
        cpuZones.push({
                        "type": name,
                        "path": zonePath + "/temp"
                      });
      } else if (name === "gpu-avg-thermal") {
        gpuAvgZonePath = zonePath + "/temp";
      } else if (/^gpu[0-9]+-?thermal$/.test(name)) {
        gpuZones.push({
                        "type": name,
                        "path": zonePath + "/temp"
                      });
      }
      currentIndex++;
      Qt.callLater(() => {
                     checkNext();
                   });
    }

    onLoadFailed: function (error) {
      currentIndex++;
      Qt.callLater(() => {
                     checkNext();
                   });
    }

    function finishScan() {
      // CPU thermal zones
      if (cpuZones.length > 0) {
        root.cpuTempSensorName = "thermal_zone";
        root.cpuThermalZonePaths = cpuZones.map(z => z.path);
        const types = cpuZones.map(z => z.type).join(", ");
        Logger.i("SystemStat", `Found ${cpuZones.length} CPU thermal zone(s): ${types}`);
      } else if (root.cpuTempHwmonPath === "") {
        Logger.w("SystemStat", "No supported temperature sensor found");
      }

      // GPU thermal zones
      if (gpuAvgZonePath !== "") {
        root.gpuThermalZonePath = gpuAvgZonePath;
        root.gpuAvailable = true;
        root.gpuType = "thermal_zone";
        Logger.i("SystemStat", `Found GPU thermal zone: gpu-avg-thermal`);
      } else if (gpuZones.length > 0) {
        root.gpuThermalZonePaths = gpuZones.map(z => z.path);
        root.gpuThermalZonePath = gpuZones[0].path; // fallback single path
        root.gpuAvailable = true;
        root.gpuType = "thermal_zone";
        const types = gpuZones.map(z => z.type).join(", ");
        Logger.i("SystemStat", `Found ${gpuZones.length} GPU thermal zone(s): ${types} (using max)`);
      }
    }
  }

  // Thermal zone reader for CPU: reads all zones, reports max (hottest core)
  FileView {
    id: cpuThermalZoneReader
    property int currentZoneIndex: 0
    property var collectedTemps: []
    printErrors: false

    onLoaded: {
      const temp = parseInt(text().trim()) / 1000.0;
      if (!isNaN(temp) && temp > 0)
      collectedTemps.push(temp);
      currentZoneIndex++;
      Qt.callLater(() => {
                     readNextCpuThermalZone();
                   });
    }

    onLoadFailed: function (error) {
      currentZoneIndex++;
      Qt.callLater(() => {
                     readNextCpuThermalZone();
                   });
    }
  }

  function readNextCpuThermalZone() {
    if (cpuThermalZoneReader.currentZoneIndex >= root.cpuThermalZonePaths.length) {
      if (cpuThermalZoneReader.collectedTemps.length > 0) {
        root.cpuTemp = Math.round(Math.max(...cpuThermalZoneReader.collectedTemps));
      } else {
        root.cpuTemp = 0;
      }
      root.pushCpuTempHistory();
      return;
    }
    cpuThermalZoneReader.path = root.cpuThermalZonePaths[cpuThermalZoneReader.currentZoneIndex];
    cpuThermalZoneReader.reload();
  }

  // Thermal zone reader for GPU: reads single zone (gpu-avg-thermal) or max of gpu[N] zones
  FileView {
    id: gpuThermalZoneReader
    property int currentZoneIndex: 0
    property var collectedTemps: []
    printErrors: false

    onLoaded: {
      const temp = parseInt(text().trim()) / 1000.0;
      if (!isNaN(temp) && temp > 0)
      collectedTemps.push(temp);

      // If we have multiple GPU zones (no gpu-avg), iterate and take max
      if (root.gpuThermalZonePaths && root.gpuThermalZonePaths.length > 0) {
        currentZoneIndex++;
        if (currentZoneIndex < root.gpuThermalZonePaths.length) {
          Qt.callLater(() => {
                         readNextGpuThermalZone();
                       });
          return;
        }
        // All zones read, take max
        root.gpuTemp = Math.round(Math.max(...collectedTemps));
      } else {
        // Single gpu-avg-thermal zone
        root.gpuTemp = Math.round(temp);
      }
      root.pushGpuHistory();
    }
  }

  function readNextGpuThermalZone() {
    gpuThermalZoneReader.path = root.gpuThermalZonePaths[gpuThermalZoneReader.currentZoneIndex];
    gpuThermalZoneReader.reload();
  }

  // Property to store multiple GPU thermal zone paths (when no gpu-avg is available)
  property var gpuThermalZonePaths: []

  // --------------------------------------------
  // --------------------------------------------
  // GPU Temperature
  // On dual-GPU systems (e.g., Intel iGPU + NVIDIA dGPU, or AMD APU + AMD dGPU),
  // we scan all hwmon entries, then select the best GPU based on priority.
  // ----
  // #1 - Scan all hwmon entries to find GPU sensors
  FileView {
    id: gpuTempNameReader
    property int currentIndex: 0
    printErrors: false

    function checkNext() {
      if (currentIndex >= 16) {
        // Finished scanning all hwmon entries
        // Only check nvidia-smi if user has explicitly enabled dGPU monitoring (opt-in)
        // because nvidia-smi wakes up the dGPU on laptops, draining battery
        if (Settings.data.systemMonitor.enableDgpuMonitoring) {
          Logger.d("SystemStat", `Found ${root.foundGpuSensors.length} sysfs GPU sensor(s), checking nvidia-smi (dGPU opt-in enabled)`);
          nvidiaSmiCheck.running = true;
        } else {
          Logger.d("SystemStat", `Found ${root.foundGpuSensors.length} sysfs GPU sensor(s), skipping nvidia-smi (dGPU opt-in disabled)`);
          root.gpuVramCheckIndex = 0;
          checkNextGpuVram();
        }
        return;
      }

      gpuTempNameReader.path = `/sys/class/hwmon/hwmon${currentIndex}/name`;
      gpuTempNameReader.reload();
    }

    onLoaded: {
      const name = text().trim();
      if (root.supportedTempGpuSensorNames.includes(name)) {
        // Collect this GPU sensor, don't stop - continue scanning for more
        const hwmonPath = `/sys/class/hwmon/hwmon${currentIndex}`;
        const gpuType = name === "amdgpu" ? "amd" : "intel";
        root.foundGpuSensors.push({
                                    "hwmonPath": hwmonPath,
                                    "type": gpuType,
                                    "hasDedicatedVram": false // Will be checked later for AMD
                                  });
        Logger.d("SystemStat", `Found ${name} GPU sensor at ${hwmonPath}`);
      }
      // Continue scanning regardless of whether we found a match
      currentIndex++;
      Qt.callLater(() => {
                     checkNext();
                   });
    }

    onLoadFailed: function (error) {
      currentIndex++;
      Qt.callLater(() => {
                     checkNext();
                   });
    }
  }

  // ----
  // #2 - Read GPU sensor value (AMD/Intel via sysfs)
  FileView {
    id: gpuTempReader
    printErrors: false

    onLoaded: {
      const data = text().trim();
      root.gpuTemp = Math.round(parseInt(data) / 1000.0);
      root.pushGpuHistory();
    }
  }

  // ----
  // #3 - Check if nvidia-smi is available (for NVIDIA GPUs)
  Process {
    id: nvidiaSmiCheck
    command: ["sh", "-c", "command -v nvidia-smi"]
    running: false
    stdout: StdioCollector {
      onStreamFinished: {
        if (text.trim().length > 0) {
          // Add NVIDIA as a GPU option (always discrete, highest priority)
          root.foundGpuSensors.push({
                                      "hwmonPath": "",
                                      "type": "nvidia",
                                      "hasDedicatedVram": true // NVIDIA is always discrete
                                    });
          Logger.d("SystemStat", "Found NVIDIA GPU (nvidia-smi available)");
        }
        // After NVIDIA check, check VRAM for AMD GPUs to distinguish dGPU from iGPU
        root.gpuVramCheckIndex = 0;
        checkNextGpuVram();
      }
    }
  }

  // ----
  // #4 - Check VRAM for AMD GPUs to distinguish dGPU from iGPU
  // dGPUs have dedicated VRAM, iGPUs don't (use system RAM)
  FileView {
    id: gpuVramChecker
    printErrors: false

    onLoaded: {
      // File exists and has content = dGPU with dedicated VRAM
      const vramSize = parseInt(text().trim());
      if (vramSize > 0) {
        root.foundGpuSensors[root.gpuVramCheckIndex].hasDedicatedVram = true;
        Logger.d("SystemStat", `GPU at ${root.foundGpuSensors[root.gpuVramCheckIndex].hwmonPath} has dedicated VRAM (dGPU)`);
      }
      root.gpuVramCheckIndex++;
      Qt.callLater(() => {
                     checkNextGpuVram();
                   });
    }

    onLoadFailed: function (error) {
      // File doesn't exist = iGPU (no dedicated VRAM)
      // hasDedicatedVram is already false by default
      root.gpuVramCheckIndex++;
      Qt.callLater(() => {
                     checkNextGpuVram();
                   });
    }
  }

  // ----
  // #4 - Read GPU temperature via nvidia-smi (NVIDIA only)
  Process {
    id: nvidiaTempProcess
    command: ["nvidia-smi", "--query-gpu=temperature.gpu", "--format=csv,noheader,nounits"]
    running: false
    stdout: StdioCollector {
      onStreamFinished: {
        const temp = parseInt(text.trim());
        if (!isNaN(temp)) {
          root.gpuTemp = temp;
          root.pushGpuHistory();
        }
      }
    }
  }

  // -------------------------------------------------------
  // -------------------------------------------------------
  // Parse ZFS ARC stats from /proc/spl/kstat/zfs/arcstats
  function parseZfsArcStats(text) {
    if (!text)
      return;
    const lines = text.split('\n');

    // The file format is: name type data
    // We need to find the lines with "size" and "c_min" and extract the values (third column)
    let foundSize = false;
    let foundCmin = false;

    for (const line of lines) {
      const parts = line.trim().split(/\s+/);
      if (parts.length >= 3) {
        if (parts[0] === 'size') {
          // The value is in bytes, convert to KB
          const arcSizeBytes = parseInt(parts[2]) || 0;
          root.zfsArcSizeKb = Math.floor(arcSizeBytes / 1024);
          foundSize = true;
        } else if (parts[0] === 'c_min') {
          // The value is in bytes, convert to KB
          const arcCminBytes = parseInt(parts[2]) || 0;
          root.zfsArcCminKb = Math.floor(arcCminBytes / 1024);
          foundCmin = true;
        }

        // If we found both, we can return early
        if (foundSize && foundCmin) {
          return;
        }
      }
    }

    // If fields not found, set to 0
    if (!foundSize) {
      root.zfsArcSizeKb = 0;
    }
    if (!foundCmin) {
      root.zfsArcCminKb = 0;
    }
  }

  // -------------------------------------------------------
  // Parse load average from /proc/loadavg
  function parseLoadAverage(text) {
    if (!text)
      return;
    const parts = text.trim().split(/\s+/);
    if (parts.length >= 3) {
      root.loadAvg1 = parseFloat(parts[0]);
      root.loadAvg5 = parseFloat(parts[1]);
      root.loadAvg15 = parseFloat(parts[2]);
    }
  }

  // -------------------------------------------------------
  // Parse memory info from /proc/meminfo
  function parseMemoryInfo(text) {
    if (!text)
      return;
    const lines = text.split('\n');
    let memTotal = 0;
    let memAvailable = 0;
    let swapTotal = 0;
    let swapFree = 0;

    for (const line of lines) {
      if (line.startsWith('MemTotal:')) {
        memTotal = parseInt(line.split(/\s+/)[1]) || 0;
      } else if (line.startsWith('MemAvailable:')) {
        memAvailable = parseInt(line.split(/\s+/)[1]) || 0;
      } else if (line.startsWith('SwapTotal:')) {
        swapTotal = parseInt(line.split(/\s+/)[1]) || 0;
      } else if (line.startsWith('SwapFree:')) {
        swapFree = parseInt(line.split(/\s+/)[1]) || 0;
      }
    }

    if (memTotal > 0) {
      // Calculate usage, adjusting for ZFS ARC cache if present
      let usageKb = memTotal - memAvailable;
      if (root.zfsArcSizeKb > 0) {
        usageKb = Math.max(0, usageKb - root.zfsArcSizeKb + root.zfsArcCminKb);
      }
      root.memGb = (usageKb / 1048576).toFixed(1); // 1024*1024 = 1048576
      root.memPercent = Math.round((usageKb / memTotal) * 100);
      root.memTotalGb = (memTotal / 1048576).toFixed(1);
      root.pushMemHistory();
    }

    // Swap usage
    root.swapTotalGb = (swapTotal / 1048576).toFixed(1);
    if (swapTotal > 0) {
      const swapUsedKb = swapTotal - swapFree;
      root.swapGb = (swapUsedKb / 1048576).toFixed(1);
      root.swapPercent = Math.round((swapUsedKb / swapTotal) * 100);
    } else {
      root.swapGb = 0;
      root.swapPercent = 0;
    }
  }

  // -------------------------------------------------------
  // Calculate CPU usage from /proc/stat

  function calculateLineUsage(line) {
    const parts = line.split(/\s+/);
    const stats = {
      "user": parseInt(parts[1]) || 0,
      "nice": parseInt(parts[2]) || 0,
      "system": parseInt(parts[3]) || 0,
      "idle": parseInt(parts[4]) || 0,
      "iowait": parseInt(parts[5]) || 0,
      "irq": parseInt(parts[6]) || 0,
      "softirq": parseInt(parts[7]) || 0,
      "steal": parseInt(parts[8]) || 0,
      "guest": parseInt(parts[9]) || 0,
      "guestNice": parseInt(parts[10]) || 0
    };
    return stats;
  }

  function computeUsage(prev, curr) {
    if (!prev || !curr)
      return -1;
    const currTotalIdle = curr.idle + curr.iowait;
    const currTotal = Object.values(curr).reduce((sum, val) => sum + val, 0);
    const prevTotalIdle = prev.idle + prev.iowait;
    const prevTotal = Object.values(prev).reduce((sum, val) => sum + val, 0);

    const diffTotal = currTotal - prevTotal;
    const diffIdle = currTotalIdle - prevTotalIdle;
    if (diffTotal > 0) {
      return (((diffTotal - diffIdle) / diffTotal) * 100).toFixed(1);
    }
    return -1;
  }

  function calculateCpuUsage(text) {
    if (!text)
      return;
    const lines = text.split('\n');
    const cpuLine = lines[0];

    // First line is total CPU
    if (!cpuLine.startsWith('cpu '))
      return;

    const currCpuStats = calculateLineUsage(cpuLine);
    const usage = computeUsage(root.prevCpuStats, currCpuStats);

    if (usage >= 0) {
      root.cpuUsage = usage;
      root.pushCpuHistory();
    }
    root.prevCpuStats = currCpuStats;

    // Find the number of CPU cores
    let nbCores = 0;
    for (let i = 1; i < lines.length; i++) {
      if (!lines[i].startsWith('cpu'))
        break;
      nbCores++;
    }

    // Fallback if we did not find any cores
    if (nbCores === 0)
      return;

    // If we found more cores than before, we reset our stats
    if (root.coresUsage.length < nbCores)
      root.coresUsage = new Array(nbCores).fill(0);

    let coresStats = [];
    let newCoresUsage = root.coresUsage.slice();
    for (let i = 0; i < nbCores; i++) {
      const coreCpuLine = lines[i + 1];
      const currCoreStats = calculateLineUsage(coreCpuLine);
      const coreUsage = computeUsage(root.prevCpuCoresStats?.[i], currCoreStats);
      if (coreUsage >= 0) {
        newCoresUsage[i] = coreUsage;
      }
      coresStats.push(currCoreStats);
    }
    root.coresUsage = newCoresUsage;
    root.prevCpuCoresStats = coresStats;
  }

  // -------------------------------------------------------
  // Check whether a network interface is virtual/tunnel/bridge.
  // Only physical interfaces (eth*, en*, wl*, ww*) are kept so
  // that traffic routed through VPNs, Docker bridges, etc. is
  // not double-counted.
  readonly property var _virtualPrefixes: ["lo", "docker", "veth", "br-", "virbr", "vnet", "tun", "tap", "wg", "tailscale", "nordlynx", "proton", "mullvad", "flannel", "cni", "cali", "vxlan", "genev", "gre", "sit", "ip6tnl", "dummy", "ifb", "nlmon", "bond"]

  function isVirtualInterface(name) {
    for (let i = 0; i < _virtualPrefixes.length; ++i) {
      if (name.startsWith(_virtualPrefixes[i]))
        return true;
    }
    return false;
  }

  // -------------------------------------------------------
  // Calculate RX and TX speed from /proc/net/dev
  // Sums speeds of all physical interfaces
  function calculateNetworkSpeed(text) {
    if (!text) {
      return;
    }

    const currentTime = Date.now() / 1000;
    const lines = text.split('\n');

    let totalRx = 0;
    let totalTx = 0;

    for (var i = 2; i < lines.length; i++) {
      const line = lines[i].trim();
      if (!line) {
        continue;
      }

      const colonIndex = line.indexOf(':');
      if (colonIndex === -1) {
        continue;
      }

      const iface = line.substring(0, colonIndex).trim();
      if (isVirtualInterface(iface)) {
        continue;
      }

      const statsLine = line.substring(colonIndex + 1).trim();
      const stats = statsLine.split(/\s+/);

      const rxBytes = parseInt(stats[0], 10) || 0;
      const txBytes = parseInt(stats[8], 10) || 0;

      totalRx += rxBytes;
      totalTx += txBytes;
    }

    // Compute only if we have a previous run to compare to.
    if (root.prevTime > 0) {
      const timeDiff = currentTime - root.prevTime;

      // Avoid division by zero if time hasn't passed.
      if (timeDiff > 0) {
        let rxDiff = totalRx - root.prevRxBytes;
        let txDiff = totalTx - root.prevTxBytes;

        // Handle counter resets (e.g., WiFi reconnect), which would cause a negative value.
        if (rxDiff < 0) {
          rxDiff = 0;
        }
        if (txDiff < 0) {
          txDiff = 0;
        }

        root.rxSpeed = Math.round(rxDiff / timeDiff); // Speed in Bytes/s
        root.txSpeed = Math.round(txDiff / timeDiff);
      }
    }

    root.prevRxBytes = totalRx;
    root.prevTxBytes = totalTx;
    root.prevTime = currentTime;

    // Update network history after speeds are computed
    root.pushNetworkHistory();
  }

  // -------------------------------------------------------
  // Helper function to format network speeds
  function formatSpeed(bytesPerSecond) {
    const units = ["KB", "MB", "GB"];
    let value = bytesPerSecond / 1000;
    let unitIndex = 0;

    while (value >= 1000 && unitIndex < units.length - 1) {
      value /= 1000;
      unitIndex++;
    }

    const unit = units[unitIndex];
    const shortUnit = unit[0];
    const numStr = value < 10 ? value.toFixed(1) : Math.round(value).toString();

    return (numStr + unit).length > 5 ? numStr + shortUnit : numStr + unit;
  }

  // -------------------------------------------------------
  // Compact speed formatter for vertical bar display
  function formatCompactSpeed(bytesPerSecond) {
    if (!bytesPerSecond || bytesPerSecond <= 0)
      return "0";
    const units = ["", "K", "M", "G"];
    let value = bytesPerSecond;
    let unitIndex = 0;
    while (value >= 1000 && unitIndex < units.length - 1) {
      value = value / 1000.0;
      unitIndex++;
    }
    // Promote at ~100 of current unit (e.g., 100k -> ~0.1M shown as 0.1M or 0M if rounded)
    if (unitIndex < units.length - 1 && value >= 100) {
      value = value / 1000.0;
      unitIndex++;
    }
    const display = Math.round(value).toString();
    return display + units[unitIndex];
  }

  // -------------------------------------------------------
  // Smart formatter for memory values (GB) - max 4 chars
  // Uses decimal for < 10GB, integer otherwise
  function formatGigabytes(memGb) {
    const value = parseFloat(memGb);
    if (isNaN(value))
      return "0G";

    if (value < 10)
      return value.toFixed(1) + "G"; // "0.0G" to "9.9G"
    return Math.round(value) + "G"; // "10G" to "999G"
  }

  // -------------------------------------------------------
  // Formatting gigabytes with optional padding
  function formatGigabytesDisplay(memGb, maxGb = null) {
    const value = formatGigabytes(memGb === null ? 0 : memGb);
    if (maxGb !== null) {
      const padding = Math.max(4, formatGigabytes(maxGb).length);
      return value.padStart(padding, " ");
    }
    return value;
  }

  // -------------------------------------------------------
  // Formatting percentage with optional padding
  function formatPercentageDisplay(value, padding = false) {
    return `${Math.round(value === null ? 0 : value)}%`.padStart(padding ? 4 : 0, " ");
  }

  // -------------------------------------------------------
  // Formatting disk usage
  function formatDiskDisplay(diskPath, {
                             percent = false,
                             available = false,
                             padding = false
} = {}) {
    if (percent) {
      const raw = available ? root.diskAvailPercents[diskPath] : root.diskPercents[diskPath];
      return formatPercentageDisplay(raw, padding);
    } else {
      const rawGb = available ? root.diskAvailableGb[diskPath] : root.diskUsedGb[diskPath];
      const maxGb = padding ? root.diskSizeGb[diskPath] : null;
      return formatGigabytesDisplay(rawGb, maxGb);
    }
  }

  // -------------------------------------------------------
  // Formatting ram usage
  function formatRamDisplay({
                            swap = false,
                            percent = false,
                            padding = false
} = {}) {
    if (percent) {
      const raw = swap ? swapPercent : memPercent;
      return formatPercentageDisplay(raw, padding);
    } else {
      const rawGb = swap ? swapGb : memGb;
      const maxGb = padding ? (swap ? swapTotalGb : memTotalGb) : null;
      return formatGigabytesDisplay(rawGb, maxGb);
    }
  }

  // -------------------------------------------------------
  // Function to start fetching and computing the cpu temperature
  function updateCpuTemperature() {
    // For AMD sensors (k10temp and zenpower), only use Tctl sensor
    // temp1_input corresponds to Tctl (Temperature Control) on these sensors
    if (root.cpuTempSensorName === "k10temp" || root.cpuTempSensorName === "zenpower") {
      cpuTempReader.path = `${root.cpuTempHwmonPath}/temp1_input`;
      cpuTempReader.reload();
    } // For Intel coretemp, start averaging all available sensors/cores
    else if (root.cpuTempSensorName === "coretemp") {
      root.intelTempValues = [];
      root.intelTempFilesChecked = 0;
      checkNextIntelTemp();
    } // For thermal_zone fallback (ARM SoCs, SCMI, etc.), read all CPU zones and take max
    else if (root.cpuTempSensorName === "thermal_zone") {
      cpuThermalZoneReader.currentZoneIndex = 0;
      cpuThermalZoneReader.collectedTemps = [];
      readNextCpuThermalZone();
    }
  }

  // -------------------------------------------------------
  // Function to check next Intel temperature sensor
  function checkNextIntelTemp() {
    if (root.intelTempFilesChecked >= root.intelTempMaxFiles) {
      // Calculate average of all found temperatures
      if (root.intelTempValues.length > 0) {
        let sum = 0;
        for (var i = 0; i < root.intelTempValues.length; i++) {
          sum += root.intelTempValues[i];
        }
        root.cpuTemp = Math.round(sum / root.intelTempValues.length);
        root.pushCpuTempHistory();
        //Logger.i("SystemStat", `Averaged ${root.intelTempValues.length} CPU thermal sensors: ${root.cpuTemp}°C`)
      } else {
        Logger.w("SystemStat", "No temperature sensors found for coretemp");
        root.cpuTemp = 0;
        root.pushCpuTempHistory();
      }
      return;
    }

    // Check next temperature file
    root.intelTempFilesChecked++;
    cpuTempReader.path = `${root.cpuTempHwmonPath}/temp${root.intelTempFilesChecked}_input`;
    cpuTempReader.reload();
  }

  // -------------------------------------------------------
  // Function to check VRAM for each AMD GPU to determine if it's a dGPU
  function checkNextGpuVram() {
    // Skip non-AMD GPUs (NVIDIA and Intel Arc are always discrete)
    while (root.gpuVramCheckIndex < root.foundGpuSensors.length) {
      const gpu = root.foundGpuSensors[root.gpuVramCheckIndex];
      if (gpu.type === "amd") {
        // Check for dedicated VRAM at hwmonPath/device/mem_info_vram_total
        gpuVramChecker.path = `${gpu.hwmonPath}/device/mem_info_vram_total`;
        gpuVramChecker.reload();
        return;
      }
      // Skip non-AMD GPUs
      root.gpuVramCheckIndex++;
    }

    // All VRAM checks complete, now select the best GPU
    selectBestGpu();
  }

  // -------------------------------------------------------
  // Function to select the best GPU based on priority
  // Priority (when dGPU monitoring enabled): NVIDIA > AMD dGPU > Intel Arc > AMD iGPU
  // Priority (when dGPU monitoring disabled): AMD iGPU only (discrete GPUs skipped to preserve D3cold)
  function selectBestGpu() {
    const dgpuEnabled = Settings.data.systemMonitor.enableDgpuMonitoring;

    if (root.foundGpuSensors.length === 0) {
      // No hwmon GPU sensors found, try thermal_zone fallback
      if (dgpuEnabled && root.gpuThermalZonePath === "" && root.gpuThermalZonePaths.length === 0) {
        // Thermal zone scanner hasn't found GPU zones yet; start a scan
        thermalZoneScanner.startScan();
      }
      return;
    }

    let best = null;

    for (var i = 0; i < root.foundGpuSensors.length; i++) {
      const gpu = root.foundGpuSensors[i];

      // NVIDIA is always highest priority (always discrete) - skip if dGPU monitoring disabled
      if (gpu.type === "nvidia") {
        if (dgpuEnabled) {
          best = gpu;
          break;
        }
        continue;
      }

      // AMD dGPU is second priority - skip if dGPU monitoring disabled (preserves D3cold power state)
      if (gpu.type === "amd" && gpu.hasDedicatedVram) {
        if (dgpuEnabled) {
          best = gpu;
          break;
        }
        continue;
      }

      // Intel Arc is third priority (always discrete) - skip if dGPU monitoring disabled
      if (gpu.type === "intel" && !best) {
        if (dgpuEnabled) {
          best = gpu;
        }
        continue;
      }

      // AMD iGPU is lowest priority (fallback) - always allowed (no D3cold issue)
      if (gpu.type === "amd" && !gpu.hasDedicatedVram && !best) {
        best = gpu;
      }
    }

    if (best) {
      root.gpuTempHwmonPath = best.hwmonPath;
      root.gpuType = best.type;
      root.gpuAvailable = true;

      const gpuDesc = best.type === "nvidia" ? "NVIDIA" : (best.type === "intel" ? "Intel Arc" : (best.hasDedicatedVram ? "AMD dGPU" : "AMD iGPU"));
      Logger.i("SystemStat", `Selected ${gpuDesc} for temperature monitoring at ${best.hwmonPath || "nvidia-smi"}`);
    } else if (!dgpuEnabled) {
      Logger.d("SystemStat", "No iGPU found and dGPU monitoring is disabled");
    }
  }

  // -------------------------------------------------------
  // Function to update GPU temperature
  function updateGpuTemperature() {
    if (root.gpuType === "nvidia") {
      nvidiaTempProcess.running = true;
    } else if (root.gpuType === "amd" || root.gpuType === "intel") {
      gpuTempReader.path = `${root.gpuTempHwmonPath}/temp1_input`;
      gpuTempReader.reload();
    } else if (root.gpuType === "thermal_zone") {
      if (root.gpuThermalZonePaths && root.gpuThermalZonePaths.length > 0) {
        // Multiple GPU zones (no gpu-avg), read all and take max
        gpuThermalZoneReader.currentZoneIndex = 0;
        gpuThermalZoneReader.collectedTemps = [];
        readNextGpuThermalZone();
      } else {
        // Single gpu-avg-thermal zone
        gpuThermalZoneReader.path = root.gpuThermalZonePath;
        gpuThermalZoneReader.reload();
      }
    }
  }
}
