pragma Singleton

import Quickshell
import Quickshell.Io
import QtQuick
import qs.Config

Singleton {
	id: root

	property string autoGpuType: "NONE"
	property string cpuName: ""
	property real cpuPerc
	property real cpuTemp

	// Individual disks: Array of { mount, used, total, free, perc }
	property var disks: []
	property real gpuMemTotal: 0
	property real gpuMemUsed
	property real gpuPerc
	property real gpuTemp
	readonly property string gpuType: Config.services.gpuType.toUpperCase() || autoGpuType
	property real lastCpuIdle
	property real lastCpuTotal
	readonly property real memPerc: memTotal > 0 ? memUsed / memTotal : 0
	property real memTotal
	property real memUsed
	property int refCount
	readonly property real storagePerc: {
		let totalUsed = 0;
		let totalSize = 0;
		for (const disk of disks) {
			totalUsed += disk.used;
			totalSize += disk.total;
		}
		return totalSize > 0 ? totalUsed / totalSize : 0;
	}

	function cleanCpuName(name: string): string {
		return name.replace(/\(R\)/gi, "").replace(/\(TM\)/gi, "").replace(/CPU/gi, "").replace(/\d+th Gen /gi, "").replace(/\d+nd Gen /gi, "").replace(/\d+rd Gen /gi, "").replace(/\d+st Gen /gi, "").replace(/Core /gi, "").replace(/Processor/gi, "").replace(/\s+/g, " ").trim();
	}

	function cleanGpuName(name: string): string {
		return name.replace(/NVIDIA GeForce /gi, "").replace(/NVIDIA /gi, "").replace(/AMD Radeon /gi, "").replace(/AMD /gi, "").replace(/Intel /gi, "").replace(/\(R\)/gi, "").replace(/\(TM\)/gi, "").replace(/Graphics/gi, "").replace(/\s+/g, " ").trim();
	}

	function formatKib(kib: real): var {
		const mib = 1024;
		const gib = 1024 ** 2;
		const tib = 1024 ** 3;

		if (kib >= tib)
			return {
				value: kib / tib,
				unit: "TiB"
			};
		if (kib >= gib)
			return {
				value: kib / gib,
				unit: "GiB"
			};
		if (kib >= mib)
			return {
				value: kib / mib,
				unit: "MiB"
			};
		return {
			value: kib,
			unit: "KiB"
		};
	}

	Timer {
		interval: Config.dashboard.resourceUpdateInterval
		repeat: true
		running: root.refCount > 0
		triggeredOnStart: true

		onTriggered: {
			stat.reload();
			meminfo.reload();
			if (root.gpuType === "GENERIC")
				gpuUsage.running = true;
		}
	}

	Timer {
		interval: 60000 * 120
		repeat: true
		running: true
		triggeredOnStart: true

		onTriggered: {
			storage.running = true;
		}
	}

	Timer {
		interval: Config.dashboard.resourceUpdateInterval * 5
		repeat: true
		running: root.refCount > 0
		triggeredOnStart: true

		onTriggered: {
			sensors.running = true;
		}
	}

	FileView {
		id: cpuinfoInit

		path: "/proc/cpuinfo"

		onLoaded: {
			const nameMatch = text().match(/model name\s*:\s*(.+)/);
			if (nameMatch)
				root.cpuName = root.cleanCpuName(nameMatch[1]);
		}
	}

	FileView {
		id: stat

		path: "/proc/stat"

		onLoaded: {
			const data = text().match(/^cpu\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/);
			if (data) {
				const stats = data.slice(1).map(n => parseInt(n, 10));
				const total = stats.reduce((a, b) => a + b, 0);
				const idle = stats[3] + (stats[4] ?? 0);

				const totalDiff = total - root.lastCpuTotal;
				const idleDiff = idle - root.lastCpuIdle;
				const newCpuPerc = totalDiff > 0 ? (1 - idleDiff / totalDiff) : 0;

				root.lastCpuTotal = total;
				root.lastCpuIdle = idle;

				if (Math.abs(newCpuPerc - root.cpuPerc) >= 0.01)
					root.cpuPerc = newCpuPerc;
			}
		}
	}

	FileView {
		id: meminfo

		path: "/proc/meminfo"

		onLoaded: {
			const data = text();
			const total = parseInt(data.match(/MemTotal: *(\d+)/)[1], 10) || 1;
			const used = (root.memTotal - parseInt(data.match(/MemAvailable: *(\d+)/)[1], 10)) || 0;

			if (root.memTotal !== total)
				root.memTotal = total;

			if (Math.abs(used - root.memUsed) >= 16384)
				root.memUsed = used;
		}
	}

	Process {
		id: storage

		command: ["lsblk", "-b", "-o", "NAME,SIZE,TYPE,FSUSED,FSSIZE", "-P"]

		stdout: StdioCollector {
			onStreamFinished: {
				const diskMap = {};  // Map disk name -> { name, totalSize, used, fsTotal }
				const lines = text.trim().split("\n");

				for (const line of lines) {
					if (line.trim() === "")
						continue;
					const nameMatch = line.match(/NAME="([^"]+)"/);
					const sizeMatch = line.match(/SIZE="([^"]+)"/);
					const typeMatch = line.match(/TYPE="([^"]+)"/);
					const fsusedMatch = line.match(/FSUSED="([^"]*)"/);
					const fssizeMatch = line.match(/FSSIZE="([^"]*)"/);

					if (!nameMatch || !typeMatch)
						continue;

					const name = nameMatch[1];
					const type = typeMatch[1];
					const size = parseInt(sizeMatch?.[1] || "0", 10);
					const fsused = parseInt(fsusedMatch?.[1] || "0", 10);
					const fssize = parseInt(fssizeMatch?.[1] || "0", 10);

					if (type === "disk") {
						// Skip zram (swap) devices
						if (name.startsWith("zram"))
							continue;

						// Initialize disk entry
						if (!diskMap[name]) {
							diskMap[name] = {
								name: name,
								totalSize: size,
								used: 0,
								fsTotal: 0
							};
						}
					} else if (type === "part") {
						// Find parent disk (remove trailing numbers/p+numbers)
						let parentDisk = name.replace(/p?\d+$/, "");
						// For nvme devices like nvme0n1p1, parent is nvme0n1
						if (name.match(/nvme\d+n\d+p\d+/))
							parentDisk = name.replace(/p\d+$/, "");

						// Aggregate partition usage to parent disk
						if (diskMap[parentDisk]) {
							diskMap[parentDisk].used += fsused;
							diskMap[parentDisk].fsTotal += fssize;
						}
					}
				}

				const diskList = [];
				let totalUsed = 0;
				let totalSize = 0;

				for (const diskName of Object.keys(diskMap).sort()) {
					const disk = diskMap[diskName];
					// Use filesystem total if available, otherwise use disk size
					const total = disk.fsTotal > 0 ? disk.fsTotal : disk.totalSize;
					const used = disk.used;
					const perc = total > 0 ? used / total : 0;

					// Convert bytes to KiB for consistency with formatKib
					diskList.push({
						mount: disk.name  // Using 'mount' property for compatibility
						,
						used: used / 1024,
						total: total / 1024,
						free: (total - used) / 1024,
						perc: perc
					});

					totalUsed += used;
					totalSize += total;
				}

				root.disks = diskList;
			}
		}
	}

	Process {
		id: gpuNameDetect

		command: ["sh", "-c", "nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null || lspci 2>/dev/null | grep -i 'vga\\|3d\\|display' | head -1"]
		running: true

		stdout: StdioCollector {
			onStreamFinished: {
				const output = text.trim();
				if (!output)
					return;

				// Check if it's from nvidia-smi (clean GPU name)
				if (output.toLowerCase().includes("nvidia") || output.toLowerCase().includes("geforce") || output.toLowerCase().includes("rtx") || output.toLowerCase().includes("gtx")) {
					root.gpuName = root.cleanGpuName(output);
				} else {
					// Parse lspci output: extract name from brackets or after colon
					const bracketMatch = output.match(/\[([^\]]+)\]/);
					if (bracketMatch) {
						root.gpuName = root.cleanGpuName(bracketMatch[1]);
					} else {
						const colonMatch = output.match(/:\s*(.+)/);
						if (colonMatch)
							root.gpuName = root.cleanGpuName(colonMatch[1]);
					}
				}
			}
		}
	}

	Process {
		id: gpuTypeCheck

		command: ["sh", "-c", "if command -v nvidia-smi &>/dev/null && nvidia-smi -L &>/dev/null; then echo NVIDIA; elif ls /sys/class/drm/card*/device/gpu_busy_percent 2>/dev/null | grep -q .; then echo GENERIC; else echo NONE; fi"]
		running: !Config.services.gpuType

		stdout: StdioCollector {
			onStreamFinished: root.autoGpuType = text.trim()
		}
	}

	Process {
		id: oneshotMem

		command: ["nvidia-smi", "--query-gpu=memory.total", "--format=csv,noheader,nounits"]
		running: root.gpuType === "NVIDIA" && root.gpuMemTotal === 0

		stdout: StdioCollector {
			onStreamFinished: {
				root.gpuMemTotal = Number(this.text.trim());
				oneshotMem.running = false;
			}
		}
	}

	Process {
		id: gpuUsageNvidia

		command: ["/usr/bin/nvidia-smi", "--query-gpu=utilization.gpu,temperature.gpu,memory.used", "--format=csv,noheader,nounits", "-lms", "1000"]
		running: root.refCount > 0 && root.gpuType === "NVIDIA"

		stdout: SplitParser {
			onRead: data => {
				const parts = String(data).trim().split(/\s*,\s*/);
				if (parts.length < 3)
					return;

				const usageRaw = parseInt(parts[0], 10);
				const tempRaw = parseInt(parts[1], 10);
				const memRaw = parseInt(parts[2], 10);

				if (!Number.isFinite(usageRaw) || !Number.isFinite(tempRaw) || !Number.isFinite(memRaw))
					return;

				const newGpuPerc = Math.max(0, Math.min(1, usageRaw / 100));
				const newGpuTemp = tempRaw;
				const newGpuMemUsed = root.gpuMemTotal > 0 ? Math.max(0, Math.min(1, memRaw / root.gpuMemTotal)) : 0;

				// Only publish meaningful changes to avoid needless binding churn / repaints
				if (Math.abs(root.gpuPerc - newGpuPerc) >= 0.01)
					root.gpuPerc = newGpuPerc;

				if (Math.abs(root.gpuTemp - newGpuTemp) >= 1)
					root.gpuTemp = newGpuTemp;

				if (Math.abs(root.gpuMemUsed - newGpuMemUsed) >= 0.01)
					root.gpuMemUsed = newGpuMemUsed;
			}
		}
	}

	Process {
		id: gpuUsage

		command: root.gpuType === "GENERIC" ? ["sh", "-c", "cat /sys/class/drm/card*/device/gpu_busy_percent"] : ["echo"]

		stdout: StdioCollector {
			onStreamFinished: {
				console.log("this is running");
				if (root.gpuType === "GENERIC") {
					const percs = text.trim().split("\n");
					const sum = percs.reduce((acc, d) => acc + parseInt(d, 10), 0);
					root.gpuPerc = sum / percs.length / 100;
				} else {
					root.gpuPerc = 0;
					root.gpuTemp = 0;
				}
			}
		}
	}

	Process {
		id: sensors

		command: ["sensors"]
		environment: ({
				LANG: "C.UTF-8",
				LC_ALL: "C.UTF-8"
			})

		stdout: StdioCollector {
			onStreamFinished: {
				let cpuTemp = text.match(/(?:Package id [0-9]+|Tdie):\s+((\+|-)[0-9.]+)(°| )C/);
				if (!cpuTemp)
					// If AMD Tdie pattern failed, try fallback on Tctl
					cpuTemp = text.match(/Tctl:\s+((\+|-)[0-9.]+)(°| )C/);

				if (cpuTemp && Math.abs(parseFloat(cpuTemp[1]) - root.cpuTemp) >= 0.5)
					root.cpuTemp = parseFloat(cpuTemp[1]);

				if (root.gpuType !== "GENERIC")
					return;

				let eligible = false;
				let sum = 0;
				let count = 0;

				for (const line of text.trim().split("\n")) {
					if (line === "Adapter: PCI adapter")
						eligible = true;
					else if (line === "")
						eligible = false;
					else if (eligible) {
						let match = line.match(/^(temp[0-9]+|GPU core|edge)+:\s+\+([0-9]+\.[0-9]+)(°| )C/);
						if (!match)
							// Fall back to junction/mem if GPU doesn't have edge temp (for AMD GPUs)
							match = line.match(/^(junction|mem)+:\s+\+([0-9]+\.[0-9]+)(°| )C/);

						if (match) {
							sum += parseFloat(match[2]);
							count++;
						}
					}
				}

				root.gpuTemp = count > 0 ? sum / count : 0;
			}
		}
	}
}
