using System.Diagnostics;
using System.Globalization;
using System.Net.NetworkInformation;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;

namespace WinBtop;

internal static class Program
{
	private static int Main(string[] args)
	{
		Console.OutputEncoding = Encoding.UTF8;
		Terminal.EnableVirtualTerminal();

		if (args.Contains("--help") || args.Contains("-h"))
		{
			Console.WriteLine("MandyTop - Windows Terminal resource monitor");
			Console.WriteLine("Usage: mandytop [--once | --json | --help | --version]");
			Console.WriteLine("  --once     print one human-readable sample and exit");
			Console.WriteLine("  --json     print one JSON sample and exit");
			Console.WriteLine("  --help     show this help");
			Console.WriteLine("  --version  show the version");
			return 0;
		}

		if (args.Contains("--version"))
		{
			Console.WriteLine("MandyTop 0.7");
			return 0;
		}

		if (args.Contains("--once") || args.Contains("--json"))
		{
			using var sampler = new Sampler(synchronousInventory: true);
			sampler.Read();
			Thread.Sleep(1000);
			var snapshot = sampler.Read();
			if (args.Contains("--json"))
			{
				Console.WriteLine(JsonSerializer.Serialize(snapshot, new JsonSerializerOptions { WriteIndented = true }));
				return 0;
			}

			Console.WriteLine($"CPU {snapshot.Cpu.TotalPercent:0.0}%");
			Console.WriteLine($"RAM {Units.Size(snapshot.Memory.UsedBytes)} / {Units.Size(snapshot.Memory.TotalBytes)} ({snapshot.Memory.UsedPercent:0.0}%)");
			Console.WriteLine($"NET down {Units.Rate(snapshot.Network.DownloadBytesPerSec)} up {Units.Rate(snapshot.Network.UploadBytesPerSec)}");
			Console.WriteLine($"UPTIME {Units.Duration(snapshot.System.Uptime)}");
			Console.WriteLine($"Processes {snapshot.Processes.Count}");
			Console.WriteLine($"USB devices {snapshot.UsbDevices.Count} drivers {snapshot.UsbDrivers.Count}");
			Console.WriteLine($"Services {snapshot.Services.Count}");
			Console.WriteLine($"Connections {snapshot.NetworkConnections.Count} startup {snapshot.StartupItems.Count} events {snapshot.Events.Count} drivers {snapshot.Drivers.Count}");
			Console.WriteLine($"Tasks {snapshot.ScheduledTasks.Count} firewall {snapshot.FirewallRules.Count} users/groups {snapshot.LocalUsers.Count}");
			Console.WriteLine($"Security {snapshot.Security.ToDisplayText()}");
			return 0;
		}

		using var app = new MonitorApp();
		return app.Run();
	}
}

internal sealed class MonitorApp : IDisposable
{
	private readonly Sampler sampler = new();
	private bool quit;
	private bool paused;
	private bool showHelp;
	private bool filtering;
	private bool treeMode;
	private ViewMode viewMode = ViewMode.Monitor;
	private string sortColumn = "cpu";
	private string processFilter = "";
	private string usbFilter = "";
	private string serviceFilter = "";
	private string networkFilter = "";
	private string startupFilter = "";
	private string eventFilter = "";
	private string driverFilter = "";
	private string taskFilter = "";
	private string firewallFilter = "";
	private string userFilter = "";
	private string filterBeforeEditing = "";
	private string statusMessage = "";
	private readonly List<LogEntry> logEntries = new();
	private bool reverseSort = true;
	private int selectedProcess;
	private int scrollOffset;
	private int selectedUsbDevice;
	private int usbScrollOffset;
	private int selectedService;
	private int serviceScrollOffset;
	private int selectedNetworkConnection;
	private int networkScrollOffset;
	private int selectedStartupItem;
	private int startupScrollOffset;
	private int selectedEvent;
	private int eventScrollOffset;
	private int selectedDriver;
	private int driverScrollOffset;
	private int selectedTask;
	private int taskScrollOffset;
	private int selectedFirewallRule;
	private int firewallScrollOffset;
	private int selectedUser;
	private int userScrollOffset;
	private PendingAction? pendingAction;
	private int intervalMs = 1000;
	private DateTime nextSample = DateTime.MinValue;
	private readonly DateTime splashUntil = DateTime.UtcNow.AddSeconds(2.2);
	private Snapshot snapshot = Snapshot.Empty;

	public int Run()
	{
		Console.CancelKeyPress += OnCancelKeyPress;
		Console.CursorVisible = false;
		Terminal.Write("\x1b[?1049h\x1b[2J\x1b[H");
		Log("MandyTop v0.7 started");

		try
		{
			while (!quit)
			{
				DrainInput();

				if (!paused && DateTime.UtcNow >= nextSample)
				{
					sampler.SetActiveView(viewMode);
					snapshot = sampler.Read();
					nextSample = DateTime.UtcNow.AddMilliseconds(intervalMs);
				}

				Draw();
				Thread.Sleep(60);
			}

			return 0;
		}
		finally
		{
			Terminal.Write("\x1b[?1049l\x1b[0m\x1b[?25h");
			Console.CursorVisible = true;
			Console.CancelKeyPress -= OnCancelKeyPress;
		}
	}

	public void Dispose()
	{
		sampler.Dispose();
	}

	private void OnCancelKeyPress(object? sender, ConsoleCancelEventArgs e)
	{
		e.Cancel = true;
		quit = true;
	}

	private void SetStatus(string message, LogLevel level = LogLevel.Info)
	{
		statusMessage = message;
		Log(message, level);
	}

	private void Log(string message, LogLevel level = LogLevel.Info)
	{
		logEntries.Add(new LogEntry(DateTime.Now, level, message));
		if (logEntries.Count > 80)
		{
			logEntries.RemoveRange(0, logEntries.Count - 80);
		}
	}

	private void DrainInput()
	{
		while (Console.KeyAvailable)
		{
			var key = Console.ReadKey(intercept: true);

			if (filtering)
			{
				var currentFilter = GetCurrentFilter();
				if (key.Key == ConsoleKey.Escape)
				{
					SetCurrentFilter(filterBeforeEditing);
					filtering = false;
					ResetSelectionForCurrentView();
					statusMessage = "Filter editing canceled";
				}
				else if (key.Key == ConsoleKey.Enter)
				{
					filtering = false;
					ResetSelectionForCurrentView();
					statusMessage = string.IsNullOrWhiteSpace(GetCurrentFilter()) ? "Filter cleared" : $"Filter: {GetCurrentFilter()}";
				}
				else if (key.Key == ConsoleKey.Backspace && currentFilter.Length > 0)
				{
					SetCurrentFilter(currentFilter[..^1]);
					ResetSelectionForCurrentView();
				}
				else if (!char.IsControl(key.KeyChar))
				{
					SetCurrentFilter(currentFilter + key.KeyChar);
					ResetSelectionForCurrentView();
				}
				continue;
			}

			if (pendingAction is not null)
			{
				if (key.Key == ConsoleKey.Enter)
				{
					ExecutePendingAction();
				}
				else if (key.Key is ConsoleKey.Escape or ConsoleKey.N)
				{
					SetStatus("Action canceled", LogLevel.Warn);
					pendingAction = null;
				}
				continue;
			}

			switch (key.Key)
			{
				case ConsoleKey.Q:
				case ConsoleKey.Escape:
					quit = true;
					break;
				case ConsoleKey.H:
				case ConsoleKey.F1:
					showHelp = !showHelp;
					break;
				case ConsoleKey.Spacebar:
					paused = !paused;
					break;
				case ConsoleKey.S:
					SaveSnapshot();
					break;
				case ConsoleKey.D1:
				case ConsoleKey.NumPad1:
					viewMode = ViewMode.Monitor;
					statusMessage = "Monitor page";
					break;
				case ConsoleKey.D2:
				case ConsoleKey.NumPad2:
					viewMode = ViewMode.Usb;
					statusMessage = "USB page";
					break;
				case ConsoleKey.D3:
				case ConsoleKey.NumPad3:
					viewMode = ViewMode.Services;
					statusMessage = "Services page";
					break;
				case ConsoleKey.D4:
				case ConsoleKey.NumPad4:
					viewMode = ViewMode.Network;
					statusMessage = "Network page";
					break;
				case ConsoleKey.D5:
				case ConsoleKey.NumPad5:
					viewMode = ViewMode.Startup;
					statusMessage = "Startup page";
					break;
				case ConsoleKey.D6:
				case ConsoleKey.NumPad6:
					viewMode = ViewMode.Events;
					statusMessage = "Eventlog page";
					break;
				case ConsoleKey.D7:
				case ConsoleKey.NumPad7:
					viewMode = ViewMode.Drivers;
					statusMessage = "Drivers page";
					break;
				case ConsoleKey.D8:
				case ConsoleKey.NumPad8:
					viewMode = ViewMode.Security;
					SetStatus("Security page");
					break;
				case ConsoleKey.D9:
				case ConsoleKey.NumPad9:
					viewMode = ViewMode.Tasks;
					SetStatus("Task Scheduler page");
					break;
				case ConsoleKey.F9:
					viewMode = ViewMode.Firewall;
					SetStatus("Firewall page");
					break;
				case ConsoleKey.F10:
					viewMode = ViewMode.Users;
					SetStatus("Users/groups page");
					break;
				case ConsoleKey.D0:
				case ConsoleKey.NumPad0:
					SetCurrentFilter("");
					ResetSelectionForCurrentView();
					statusMessage = "Filter cleared";
					break;
				case ConsoleKey.F:
				case ConsoleKey.Oem2:
				case ConsoleKey.Divide:
					filterBeforeEditing = GetCurrentFilter();
					filtering = true;
					statusMessage = "Type filter, Enter to apply, Esc to cancel";
					break;
				case ConsoleKey.Add:
				case ConsoleKey.OemPlus:
					intervalMs = Math.Min(10_000, intervalMs + 250);
					break;
				case ConsoleKey.Subtract:
				case ConsoleKey.OemMinus:
					intervalMs = Math.Max(250, intervalMs - 250);
					break;
				default:
					HandleViewKey(key);
					break;
			}
		}
	}

	private void HandleViewKey(ConsoleKeyInfo key)
	{
		switch (viewMode)
		{
			case ViewMode.Usb:
				HandleUsbKey(key);
				break;
			case ViewMode.Services:
				HandleServicesKey(key);
				break;
			case ViewMode.Network:
				HandleNetworkKey(key);
				break;
			case ViewMode.Startup:
				HandleStartupKey(key);
				break;
			case ViewMode.Events:
				HandleEventsKey(key);
				break;
			case ViewMode.Drivers:
				HandleDriversKey(key);
				break;
			case ViewMode.Security:
				HandleSecurityKey(key);
				break;
			case ViewMode.Tasks:
				HandleTasksKey(key);
				break;
			case ViewMode.Firewall:
				HandleFirewallKey(key);
				break;
			case ViewMode.Users:
				HandleUsersKey(key);
				break;
			default:
				HandleMonitorKey(key);
				break;
		}
	}

	private void HandleMonitorKey(ConsoleKeyInfo key)
	{
		switch (key.Key)
		{
			case ConsoleKey.R:
				reverseSort = !reverseSort;
				break;
			case ConsoleKey.E:
				ExportProcessesCsv();
				break;
			case ConsoleKey.C:
				CopySelectedProcess();
				break;
			case ConsoleKey.T:
				treeMode = !treeMode;
				selectedProcess = 0;
				scrollOffset = 0;
				statusMessage = treeMode ? "Process tree enabled" : "Process tree disabled";
				break;
			case ConsoleKey.P:
				ArmPriorityChange("AboveNormal");
				break;
			case ConsoleKey.N:
				ArmPriorityChange("Normal");
				break;
			case ConsoleKey.B:
				ArmProcessAction(PendingActionKind.KillProcessTree);
				break;
			case ConsoleKey.Z:
				ArmProcessAction(PendingActionKind.SuspendProcess);
				break;
			case ConsoleKey.U:
				ArmProcessAction(PendingActionKind.ResumeProcess);
				break;
			case ConsoleKey.Tab:
				sortColumn = sortColumn switch
				{
					"cpu" => "mem",
					"mem" => "threads",
					"threads" => "pid",
					"pid" => "name",
					_ => "cpu"
				};
				selectedProcess = 0;
				scrollOffset = 0;
				break;
			case ConsoleKey.Delete:
			case ConsoleKey.X:
				ArmKillForSelectedProcess();
				break;
			case ConsoleKey.UpArrow:
			case ConsoleKey.K:
				selectedProcess = Math.Max(0, selectedProcess - 1);
				break;
			case ConsoleKey.DownArrow:
			case ConsoleKey.J:
				selectedProcess = Math.Min(Math.Max(0, GetVisibleProcesses().Count - 1), selectedProcess + 1);
				break;
			case ConsoleKey.Home:
				selectedProcess = 0;
				break;
			case ConsoleKey.End:
				selectedProcess = Math.Max(0, GetVisibleProcesses().Count - 1);
				break;
			case ConsoleKey.PageUp:
				selectedProcess = Math.Max(0, selectedProcess - 10);
				break;
			case ConsoleKey.PageDown:
				selectedProcess = Math.Min(Math.Max(0, GetVisibleProcesses().Count - 1), selectedProcess + 10);
				break;
		}
	}

	private void HandleNetworkKey(ConsoleKeyInfo key)
	{
		switch (key.Key)
		{
			case ConsoleKey.R:
				sampler.RefreshInventory();
				nextSample = DateTime.MinValue;
				statusMessage = "Network refresh queued";
				break;
			case ConsoleKey.E:
				ExportNetworkCsv();
				break;
			case ConsoleKey.C:
				CopySelectedNetworkConnection();
				break;
			case ConsoleKey.Delete:
			case ConsoleKey.X:
				ArmKillForSelectedNetworkConnection();
				break;
			case ConsoleKey.UpArrow:
			case ConsoleKey.K:
				selectedNetworkConnection = Math.Max(0, selectedNetworkConnection - 1);
				break;
			case ConsoleKey.DownArrow:
			case ConsoleKey.J:
				selectedNetworkConnection = Math.Min(Math.Max(0, GetVisibleNetworkConnections().Count - 1), selectedNetworkConnection + 1);
				break;
			case ConsoleKey.Home:
				selectedNetworkConnection = 0;
				break;
			case ConsoleKey.End:
				selectedNetworkConnection = Math.Max(0, GetVisibleNetworkConnections().Count - 1);
				break;
			case ConsoleKey.PageUp:
				selectedNetworkConnection = Math.Max(0, selectedNetworkConnection - 10);
				break;
			case ConsoleKey.PageDown:
				selectedNetworkConnection = Math.Min(Math.Max(0, GetVisibleNetworkConnections().Count - 1), selectedNetworkConnection + 10);
				break;
		}
	}

	private void HandleStartupKey(ConsoleKeyInfo key)
	{
		switch (key.Key)
		{
			case ConsoleKey.R:
				sampler.RefreshInventory();
				nextSample = DateTime.MinValue;
				statusMessage = "Startup refresh queued";
				break;
			case ConsoleKey.E:
				ExportStartupCsv();
				break;
			case ConsoleKey.C:
				CopySelectedStartupItem();
				break;
			case ConsoleKey.UpArrow:
			case ConsoleKey.K:
				selectedStartupItem = Math.Max(0, selectedStartupItem - 1);
				break;
			case ConsoleKey.DownArrow:
			case ConsoleKey.J:
				selectedStartupItem = Math.Min(Math.Max(0, GetVisibleStartupItems().Count - 1), selectedStartupItem + 1);
				break;
			case ConsoleKey.Home:
				selectedStartupItem = 0;
				break;
			case ConsoleKey.End:
				selectedStartupItem = Math.Max(0, GetVisibleStartupItems().Count - 1);
				break;
			case ConsoleKey.PageUp:
				selectedStartupItem = Math.Max(0, selectedStartupItem - 10);
				break;
			case ConsoleKey.PageDown:
				selectedStartupItem = Math.Min(Math.Max(0, GetVisibleStartupItems().Count - 1), selectedStartupItem + 10);
				break;
		}
	}

	private void HandleEventsKey(ConsoleKeyInfo key)
	{
		switch (key.Key)
		{
			case ConsoleKey.R:
				sampler.RefreshInventory();
				nextSample = DateTime.MinValue;
				statusMessage = "Eventlog refresh queued";
				break;
			case ConsoleKey.E:
				ExportEventsCsv();
				break;
			case ConsoleKey.C:
				CopySelectedEvent();
				break;
			case ConsoleKey.UpArrow:
			case ConsoleKey.K:
				selectedEvent = Math.Max(0, selectedEvent - 1);
				break;
			case ConsoleKey.DownArrow:
			case ConsoleKey.J:
				selectedEvent = Math.Min(Math.Max(0, GetVisibleEvents().Count - 1), selectedEvent + 1);
				break;
			case ConsoleKey.Home:
				selectedEvent = 0;
				break;
			case ConsoleKey.End:
				selectedEvent = Math.Max(0, GetVisibleEvents().Count - 1);
				break;
			case ConsoleKey.PageUp:
				selectedEvent = Math.Max(0, selectedEvent - 10);
				break;
			case ConsoleKey.PageDown:
				selectedEvent = Math.Min(Math.Max(0, GetVisibleEvents().Count - 1), selectedEvent + 10);
				break;
		}
	}

	private void HandleDriversKey(ConsoleKeyInfo key)
	{
		switch (key.Key)
		{
			case ConsoleKey.R:
				sampler.RefreshInventory();
				nextSample = DateTime.MinValue;
				statusMessage = "Driver refresh queued";
				break;
			case ConsoleKey.E:
				ExportDriversCsv();
				break;
			case ConsoleKey.C:
				CopySelectedDriver();
				break;
			case ConsoleKey.O:
			case ConsoleKey.Delete:
			case ConsoleKey.X:
				ArmDriverAction(PendingActionKind.StopService);
				break;
			case ConsoleKey.A:
				ArmDriverAction(PendingActionKind.StartService);
				break;
			case ConsoleKey.UpArrow:
			case ConsoleKey.K:
				selectedDriver = Math.Max(0, selectedDriver - 1);
				break;
			case ConsoleKey.DownArrow:
			case ConsoleKey.J:
				selectedDriver = Math.Min(Math.Max(0, GetVisibleDrivers().Count - 1), selectedDriver + 1);
				break;
			case ConsoleKey.Home:
				selectedDriver = 0;
				break;
			case ConsoleKey.End:
				selectedDriver = Math.Max(0, GetVisibleDrivers().Count - 1);
				break;
			case ConsoleKey.PageUp:
				selectedDriver = Math.Max(0, selectedDriver - 10);
				break;
			case ConsoleKey.PageDown:
				selectedDriver = Math.Min(Math.Max(0, GetVisibleDrivers().Count - 1), selectedDriver + 10);
				break;
		}
	}

	private void HandleSecurityKey(ConsoleKeyInfo key)
	{
		switch (key.Key)
		{
			case ConsoleKey.R:
				sampler.RefreshInventory();
				nextSample = DateTime.MinValue;
				statusMessage = "Security refresh queued";
				break;
			case ConsoleKey.E:
				ExportSecurityCsv();
				break;
			case ConsoleKey.C:
				CopyToClipboard(snapshot.Security.ToDisplayText(), "Copied security summary");
				break;
		}
	}

	private void HandleTasksKey(ConsoleKeyInfo key)
	{
		switch (key.Key)
		{
			case ConsoleKey.R:
				sampler.RefreshInventory();
				nextSample = DateTime.MinValue;
				SetStatus("Task refresh queued");
				break;
			case ConsoleKey.E:
				ExportTasksCsv();
				break;
			case ConsoleKey.C:
				CopySelectedTask();
				break;
			case ConsoleKey.A:
				ArmTaskAction(PendingActionKind.EnableTask);
				break;
			case ConsoleKey.D:
			case ConsoleKey.Delete:
				ArmTaskAction(PendingActionKind.DisableTask);
				break;
			case ConsoleKey.S:
			case ConsoleKey.X:
				ArmTaskAction(PendingActionKind.StartTask);
				break;
			case ConsoleKey.UpArrow:
			case ConsoleKey.K:
				selectedTask = Math.Max(0, selectedTask - 1);
				break;
			case ConsoleKey.DownArrow:
			case ConsoleKey.J:
				selectedTask = Math.Min(Math.Max(0, GetVisibleTasks().Count - 1), selectedTask + 1);
				break;
			case ConsoleKey.Home:
				selectedTask = 0;
				break;
			case ConsoleKey.End:
				selectedTask = Math.Max(0, GetVisibleTasks().Count - 1);
				break;
			case ConsoleKey.PageUp:
				selectedTask = Math.Max(0, selectedTask - 10);
				break;
			case ConsoleKey.PageDown:
				selectedTask = Math.Min(Math.Max(0, GetVisibleTasks().Count - 1), selectedTask + 10);
				break;
		}
	}

	private void HandleFirewallKey(ConsoleKeyInfo key)
	{
		switch (key.Key)
		{
			case ConsoleKey.R:
				sampler.RefreshInventory();
				nextSample = DateTime.MinValue;
				SetStatus("Firewall refresh queued");
				break;
			case ConsoleKey.E:
				ExportFirewallCsv();
				break;
			case ConsoleKey.C:
				CopySelectedFirewallRule();
				break;
			case ConsoleKey.A:
				ArmFirewallAction(PendingActionKind.EnableFirewallRule);
				break;
			case ConsoleKey.D:
			case ConsoleKey.Delete:
			case ConsoleKey.X:
				ArmFirewallAction(PendingActionKind.DisableFirewallRule);
				break;
			case ConsoleKey.UpArrow:
			case ConsoleKey.K:
				selectedFirewallRule = Math.Max(0, selectedFirewallRule - 1);
				break;
			case ConsoleKey.DownArrow:
			case ConsoleKey.J:
				selectedFirewallRule = Math.Min(Math.Max(0, GetVisibleFirewallRules().Count - 1), selectedFirewallRule + 1);
				break;
			case ConsoleKey.Home:
				selectedFirewallRule = 0;
				break;
			case ConsoleKey.End:
				selectedFirewallRule = Math.Max(0, GetVisibleFirewallRules().Count - 1);
				break;
			case ConsoleKey.PageUp:
				selectedFirewallRule = Math.Max(0, selectedFirewallRule - 10);
				break;
			case ConsoleKey.PageDown:
				selectedFirewallRule = Math.Min(Math.Max(0, GetVisibleFirewallRules().Count - 1), selectedFirewallRule + 10);
				break;
		}
	}

	private void HandleUsersKey(ConsoleKeyInfo key)
	{
		switch (key.Key)
		{
			case ConsoleKey.R:
				sampler.RefreshInventory();
				nextSample = DateTime.MinValue;
				SetStatus("Users refresh queued");
				break;
			case ConsoleKey.E:
				ExportUsersCsv();
				break;
			case ConsoleKey.C:
				CopySelectedUser();
				break;
			case ConsoleKey.UpArrow:
			case ConsoleKey.K:
				selectedUser = Math.Max(0, selectedUser - 1);
				break;
			case ConsoleKey.DownArrow:
			case ConsoleKey.J:
				selectedUser = Math.Min(Math.Max(0, GetVisibleUsers().Count - 1), selectedUser + 1);
				break;
			case ConsoleKey.Home:
				selectedUser = 0;
				break;
			case ConsoleKey.End:
				selectedUser = Math.Max(0, GetVisibleUsers().Count - 1);
				break;
			case ConsoleKey.PageUp:
				selectedUser = Math.Max(0, selectedUser - 10);
				break;
			case ConsoleKey.PageDown:
				selectedUser = Math.Min(Math.Max(0, GetVisibleUsers().Count - 1), selectedUser + 10);
				break;
		}
	}

	private void HandleUsbKey(ConsoleKeyInfo key)
	{
		switch (key.Key)
		{
			case ConsoleKey.R:
				sampler.RefreshInventory();
				nextSample = DateTime.MinValue;
				statusMessage = "USB inventory refresh queued";
				break;
			case ConsoleKey.E:
				ExportUsbCsv();
				break;
			case ConsoleKey.C:
				CopySelectedUsbDevice();
				break;
			case ConsoleKey.D:
			case ConsoleKey.Delete:
			case ConsoleKey.X:
				ArmDeviceAction(PendingActionKind.DisableDevice);
				break;
			case ConsoleKey.A:
				ArmDeviceAction(PendingActionKind.EnableDevice);
				break;
			case ConsoleKey.UpArrow:
			case ConsoleKey.K:
				selectedUsbDevice = Math.Max(0, selectedUsbDevice - 1);
				break;
			case ConsoleKey.DownArrow:
			case ConsoleKey.J:
				selectedUsbDevice = Math.Min(Math.Max(0, GetVisibleUsbDevices().Count - 1), selectedUsbDevice + 1);
				break;
			case ConsoleKey.Home:
				selectedUsbDevice = 0;
				break;
			case ConsoleKey.End:
				selectedUsbDevice = Math.Max(0, GetVisibleUsbDevices().Count - 1);
				break;
			case ConsoleKey.PageUp:
				selectedUsbDevice = Math.Max(0, selectedUsbDevice - 10);
				break;
			case ConsoleKey.PageDown:
				selectedUsbDevice = Math.Min(Math.Max(0, GetVisibleUsbDevices().Count - 1), selectedUsbDevice + 10);
				break;
		}
	}

	private void HandleServicesKey(ConsoleKeyInfo key)
	{
		switch (key.Key)
		{
			case ConsoleKey.R:
				ArmServiceAction(PendingActionKind.RestartService);
				break;
			case ConsoleKey.O:
			case ConsoleKey.Delete:
			case ConsoleKey.X:
				ArmServiceAction(PendingActionKind.StopService);
				break;
			case ConsoleKey.A:
				ArmServiceAction(PendingActionKind.StartService);
				break;
			case ConsoleKey.E:
				ExportServicesCsv();
				break;
			case ConsoleKey.C:
				CopySelectedService();
				break;
			case ConsoleKey.Tab:
				sampler.RefreshInventory();
				nextSample = DateTime.MinValue;
				statusMessage = "Service refresh queued";
				break;
			case ConsoleKey.UpArrow:
			case ConsoleKey.K:
				selectedService = Math.Max(0, selectedService - 1);
				break;
			case ConsoleKey.DownArrow:
			case ConsoleKey.J:
				selectedService = Math.Min(Math.Max(0, GetVisibleServices().Count - 1), selectedService + 1);
				break;
			case ConsoleKey.Home:
				selectedService = 0;
				break;
			case ConsoleKey.End:
				selectedService = Math.Max(0, GetVisibleServices().Count - 1);
				break;
			case ConsoleKey.PageUp:
				selectedService = Math.Max(0, selectedService - 10);
				break;
			case ConsoleKey.PageDown:
				selectedService = Math.Min(Math.Max(0, GetVisibleServices().Count - 1), selectedService + 10);
				break;
		}
	}

	private string GetCurrentFilter()
	{
		return viewMode switch
		{
			ViewMode.Usb => usbFilter,
			ViewMode.Services => serviceFilter,
			ViewMode.Network => networkFilter,
			ViewMode.Startup => startupFilter,
			ViewMode.Events => eventFilter,
			ViewMode.Drivers => driverFilter,
			ViewMode.Tasks => taskFilter,
			ViewMode.Firewall => firewallFilter,
			ViewMode.Users => userFilter,
			_ => processFilter
		};
	}

	private void SetCurrentFilter(string value)
	{
		switch (viewMode)
		{
			case ViewMode.Usb:
				usbFilter = value;
				break;
			case ViewMode.Services:
				serviceFilter = value;
				break;
			case ViewMode.Network:
				networkFilter = value;
				break;
			case ViewMode.Startup:
				startupFilter = value;
				break;
			case ViewMode.Events:
				eventFilter = value;
				break;
			case ViewMode.Drivers:
				driverFilter = value;
				break;
			case ViewMode.Tasks:
				taskFilter = value;
				break;
			case ViewMode.Firewall:
				firewallFilter = value;
				break;
			case ViewMode.Users:
				userFilter = value;
				break;
			default:
				processFilter = value;
				break;
		}
	}

	private void ResetSelectionForCurrentView()
	{
		switch (viewMode)
		{
			case ViewMode.Usb:
				selectedUsbDevice = 0;
				usbScrollOffset = 0;
				break;
			case ViewMode.Services:
				selectedService = 0;
				serviceScrollOffset = 0;
				break;
			case ViewMode.Network:
				selectedNetworkConnection = 0;
				networkScrollOffset = 0;
				break;
			case ViewMode.Startup:
				selectedStartupItem = 0;
				startupScrollOffset = 0;
				break;
			case ViewMode.Events:
				selectedEvent = 0;
				eventScrollOffset = 0;
				break;
			case ViewMode.Drivers:
				selectedDriver = 0;
				driverScrollOffset = 0;
				break;
			case ViewMode.Tasks:
				selectedTask = 0;
				taskScrollOffset = 0;
				break;
			case ViewMode.Firewall:
				selectedFirewallRule = 0;
				firewallScrollOffset = 0;
				break;
			case ViewMode.Users:
				selectedUser = 0;
				userScrollOffset = 0;
				break;
			default:
				selectedProcess = 0;
				scrollOffset = 0;
				break;
		}
	}

	private void SaveSnapshot()
	{
		try
		{
			var filename = $"winbtop-snapshot-{DateTime.Now:yyyyMMdd-HHmmss}.json";
			var options = new JsonSerializerOptions { WriteIndented = true };
			File.WriteAllText(filename, JsonSerializer.Serialize(snapshot, options));
			SetStatus($"Saved {filename}");
		}
		catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or NotSupportedException)
		{
			SetStatus($"Could not save snapshot: {ex.Message}", LogLevel.Error);
		}
	}

	private void ExportProcessesCsv()
	{
		try
		{
			var filename = $"winbtop-processes-{DateTime.Now:yyyyMMdd-HHmmss}.csv";
			var lines = new List<string> { "PID,ParentPID,Name,CPUPercent,MemoryBytes,Threads,Handles,Priority,Responding,StartTime,Path" };
			lines.AddRange(GetVisibleProcesses().Select(p => string.Join(",",
				p.Pid,
				p.ParentPid,
				Csv(p.Name),
				p.CpuPercent.ToString("0.00", CultureInfo.InvariantCulture),
				p.MemoryBytes,
				p.ThreadCount,
				p.HandleCount,
				Csv(p.Priority),
				p.Responding,
				Csv(p.StartTime?.ToString("O") ?? ""),
				Csv(p.Path ?? ""))));
			File.WriteAllLines(filename, lines, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true));
			SetStatus($"Exported {filename}");
		}
		catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or NotSupportedException)
		{
			SetStatus($"Could not export CSV: {ex.Message}", LogLevel.Error);
		}
	}

	private void ExportUsbCsv()
	{
		try
		{
			var filename = $"winbtop-usb-{DateTime.Now:yyyyMMdd-HHmmss}.csv";
			var lines = new List<string> { "Kind,Name,Status,Class,Present,Provider,Version,InfName,InstanceOrDeviceId" };
			lines.AddRange(GetVisibleUsbDevices().Select(d => string.Join(",",
				"Device",
				Csv(d.Name),
				Csv(d.Status),
				Csv(d.Class),
				d.Present,
				"",
				"",
				"",
				Csv(d.InstanceId))));
			lines.AddRange(snapshot.UsbDrivers.Select(d => string.Join(",",
				"Driver",
				Csv(d.DeviceName),
				"",
				Csv(d.DeviceClass),
				"",
				Csv(d.Provider),
				Csv(d.Version),
				Csv(d.InfName),
				Csv(d.DeviceId))));
			File.WriteAllLines(filename, lines, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true));
			SetStatus($"Exported {filename}");
		}
		catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or NotSupportedException)
		{
			SetStatus($"Could not export USB CSV: {ex.Message}", LogLevel.Error);
		}
	}

	private void ExportServicesCsv()
	{
		try
		{
			var filename = $"mandytop-services-{DateTime.Now:yyyyMMdd-HHmmss}.csv";
			var lines = new List<string> { "Name,DisplayName,State,StartMode,AcceptStop,ProcessId,DependsOn,DependedOnBy,Path" };
			lines.AddRange(GetVisibleServices().Select(s => string.Join(",",
				Csv(s.Name),
				Csv(s.DisplayName),
				Csv(s.State),
				Csv(s.StartMode),
				s.AcceptStop,
				s.ProcessId,
				Csv(s.DependsOn),
				Csv(s.DependedOnBy),
				Csv(s.Path ?? ""))));
			File.WriteAllLines(filename, lines, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true));
			SetStatus($"Exported {filename}");
		}
		catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or NotSupportedException)
		{
			SetStatus($"Could not export services CSV: {ex.Message}", LogLevel.Error);
		}
	}

	private void ExportNetworkCsv()
	{
		ExportCsv("mandytop-network", "Protocol,State,Local,Remote,PID,Process", GetVisibleNetworkConnections().Select(c => string.Join(",",
			Csv(c.Protocol),
			Csv(c.State),
			Csv(c.LocalEndpoint),
			Csv(c.RemoteEndpoint),
			c.ProcessId,
			Csv(c.ProcessName))));
	}

	private void ExportStartupCsv()
	{
		ExportCsv("mandytop-startup", "Name,Source,Location,Command", GetVisibleStartupItems().Select(i => string.Join(",",
			Csv(i.Name),
			Csv(i.Source),
			Csv(i.Location),
			Csv(i.Command))));
	}

	private void ExportEventsCsv()
	{
		ExportCsv("mandytop-events", "TimeCreated,LogName,Level,Provider,Id,Message", GetVisibleEvents().Select(e => string.Join(",",
			Csv(e.TimeCreated?.ToString("O") ?? ""),
			Csv(e.LogName),
			Csv(e.Level),
			Csv(e.Provider),
			e.Id,
			Csv(e.Message))));
	}

	private void ExportDriversCsv()
	{
		ExportCsv("mandytop-drivers", "Name,DisplayName,State,StartMode,Path", GetVisibleDrivers().Select(d => string.Join(",",
			Csv(d.Name),
			Csv(d.DisplayName),
			Csv(d.State),
			Csv(d.StartMode),
			Csv(d.Path ?? ""))));
	}

	private void ExportSecurityCsv()
	{
		var security = snapshot.Security;
		var rows = new[]
		{
			$"DefenderRealTime,{Csv(security.DefenderRealTime)}",
			$"DefenderAntivirus,{Csv(security.DefenderAntivirus)}",
			$"DefenderSignatureAge,{security.DefenderSignatureAgeDays}",
			$"FirewallDomain,{Csv(security.FirewallDomain)}",
			$"FirewallPrivate,{Csv(security.FirewallPrivate)}",
			$"FirewallPublic,{Csv(security.FirewallPublic)}",
			$"LatestHotFix,{Csv(security.LatestHotFix)}",
			$"PendingReboot,{security.PendingReboot}",
			$"GpuSummary,{Csv(security.GpuSummary)}",
			$"ThermalSummary,{Csv(security.ThermalSummary)}"
		};
		ExportCsv("mandytop-security", "Name,Value", rows);
	}

	private void ExportTasksCsv()
	{
		ExportCsv("mandytop-tasks", "TaskPath,TaskName,State,Enabled,LastRunTime,NextRunTime,Action", GetVisibleTasks().Select(t => string.Join(",",
			Csv(t.TaskPath),
			Csv(t.TaskName),
			Csv(t.State),
			t.Enabled,
			Csv(t.LastRunTime?.ToString("O") ?? ""),
			Csv(t.NextRunTime?.ToString("O") ?? ""),
			Csv(t.Action))));
	}

	private void ExportFirewallCsv()
	{
		ExportCsv("mandytop-firewall", "Name,DisplayName,Enabled,Direction,Action,Profile,Group", GetVisibleFirewallRules().Select(r => string.Join(",",
			Csv(r.Name),
			Csv(r.DisplayName),
			r.Enabled,
			Csv(r.Direction),
			Csv(r.Action),
			Csv(r.Profile),
			Csv(r.Group))));
	}

	private void ExportUsersCsv()
	{
		ExportCsv("mandytop-users", "Kind,Name,Enabled,IsAdmin,LastLogon,Description,Members", GetVisibleUsers().Select(u => string.Join(",",
			Csv(u.Kind),
			Csv(u.Name),
			u.Enabled,
			u.IsAdmin,
			Csv(u.LastLogon?.ToString("O") ?? ""),
			Csv(u.Description ?? ""),
			Csv(u.Members))));
	}

	private void ExportCsv(string prefix, string header, IEnumerable<string> rows)
	{
		try
		{
			var filename = $"{prefix}-{DateTime.Now:yyyyMMdd-HHmmss}.csv";
			File.WriteAllLines(filename, new[] { header }.Concat(rows), new UTF8Encoding(encoderShouldEmitUTF8Identifier: true));
			SetStatus($"Exported {filename}");
		}
		catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or NotSupportedException)
		{
			SetStatus($"Could not export CSV: {ex.Message}", LogLevel.Error);
		}
	}

	private void CopySelectedProcess()
	{
		var selected = GetSelectedProcess();
		if (selected is null)
		{
			statusMessage = "No process selected";
			return;
		}

		try
		{
			var value = selected.Value.Path ?? $"{selected.Value.Name} (PID {selected.Value.Pid})";
			using var clip = Process.Start(new ProcessStartInfo("clip.exe")
			{
				RedirectStandardInput = true,
				UseShellExecute = false,
				CreateNoWindow = true
			});
			if (clip is null)
			{
				throw new InvalidOperationException("clip.exe could not be started");
			}
			clip.StandardInput.Write(value);
			clip.StandardInput.Close();
			clip.WaitForExit(2000);
			statusMessage = $"Copied PID {selected.Value.Pid} details";
		}
		catch (Exception ex) when (ex is InvalidOperationException or System.ComponentModel.Win32Exception)
		{
			statusMessage = $"Could not copy: {ex.Message}";
		}
	}

	private void CopySelectedUsbDevice()
	{
		var selected = GetSelectedUsbDevice();
		if (selected is null)
		{
			statusMessage = "No USB device selected";
			return;
		}

		CopyToClipboard(selected.Value.InstanceId, $"Copied USB instance ID");
	}

	private void CopySelectedService()
	{
		var selected = GetSelectedService();
		if (selected is null)
		{
			statusMessage = "No service selected";
			return;
		}

		CopyToClipboard(selected.Value.Name, $"Copied service name {selected.Value.Name}");
	}

	private void CopySelectedNetworkConnection()
	{
		var selected = GetSelectedNetworkConnection();
		if (selected is null)
		{
			statusMessage = "No network connection selected";
			return;
		}

		CopyToClipboard($"{selected.Value.Protocol} {selected.Value.LocalEndpoint} -> {selected.Value.RemoteEndpoint} PID {selected.Value.ProcessId}", "Copied network connection");
	}

	private void CopySelectedStartupItem()
	{
		var selected = GetSelectedStartupItem();
		if (selected is null)
		{
			statusMessage = "No startup item selected";
			return;
		}

		CopyToClipboard(selected.Value.Command, $"Copied startup command {selected.Value.Name}");
	}

	private void CopySelectedEvent()
	{
		var selected = GetSelectedEvent();
		if (selected is null)
		{
			statusMessage = "No event selected";
			return;
		}

		CopyToClipboard($"{selected.Value.TimeCreated:O} {selected.Value.LogName} {selected.Value.Level} {selected.Value.Provider} {selected.Value.Id}: {selected.Value.Message}", "Copied event");
	}

	private void CopySelectedDriver()
	{
		var selected = GetSelectedDriver();
		if (selected is null)
		{
			statusMessage = "No driver selected";
			return;
		}

		CopyToClipboard(selected.Value.Name, $"Copied driver name {selected.Value.Name}");
	}

	private void CopySelectedTask()
	{
		var selected = GetSelectedTask();
		if (selected is null)
		{
			SetStatus("No task selected", LogLevel.Warn);
			return;
		}

		CopyToClipboard($"{selected.Value.TaskPath}{selected.Value.TaskName}", $"Copied task {selected.Value.TaskName}");
	}

	private void CopySelectedFirewallRule()
	{
		var selected = GetSelectedFirewallRule();
		if (selected is null)
		{
			SetStatus("No firewall rule selected", LogLevel.Warn);
			return;
		}

		CopyToClipboard(selected.Value.Name, $"Copied firewall rule {selected.Value.DisplayName}");
	}

	private void CopySelectedUser()
	{
		var selected = GetSelectedUser();
		if (selected is null)
		{
			SetStatus("No user/group selected", LogLevel.Warn);
			return;
		}

		CopyToClipboard($"{selected.Value.Kind} {selected.Value.Name} admin={selected.Value.IsAdmin} enabled={selected.Value.Enabled} lastLogon={selected.Value.LastLogon}", $"Copied {selected.Value.Name}");
	}

	private void CopyToClipboard(string value, string successMessage)
	{
		try
		{
			using var clip = Process.Start(new ProcessStartInfo("clip.exe")
			{
				RedirectStandardInput = true,
				UseShellExecute = false,
				CreateNoWindow = true
			});
			if (clip is null)
			{
				throw new InvalidOperationException("clip.exe could not be started");
			}
			clip.StandardInput.Write(value);
			clip.StandardInput.Close();
			clip.WaitForExit(2000);
			SetStatus(successMessage);
		}
		catch (Exception ex) when (ex is InvalidOperationException or System.ComponentModel.Win32Exception)
		{
			SetStatus($"Could not copy: {ex.Message}", LogLevel.Error);
		}
	}

	private static string Csv(string value) => $"\"{value.Replace("\"", "\"\"")}\"";
	private static string Blank(string value) => string.IsNullOrWhiteSpace(value) ? "none" : value;

	private List<UsbDeviceSnapshot> GetVisibleUsbDevices()
	{
		IEnumerable<UsbDeviceSnapshot> devices = snapshot.UsbDevices;
		if (!string.IsNullOrWhiteSpace(usbFilter))
		{
			devices = devices.Where(d =>
				d.Name.Contains(usbFilter, StringComparison.OrdinalIgnoreCase) ||
				d.Status.Contains(usbFilter, StringComparison.OrdinalIgnoreCase) ||
				d.Class.Contains(usbFilter, StringComparison.OrdinalIgnoreCase) ||
				d.InstanceId.Contains(usbFilter, StringComparison.OrdinalIgnoreCase));
		}

		return devices
			.OrderByDescending(d => d.Present)
			.ThenBy(d => d.Class)
			.ThenBy(d => d.Name)
			.ToList();
	}

	private List<ServiceSnapshot> GetVisibleServices()
	{
		IEnumerable<ServiceSnapshot> services = snapshot.Services;
		if (!string.IsNullOrWhiteSpace(serviceFilter))
		{
			services = services.Where(s =>
				s.Name.Contains(serviceFilter, StringComparison.OrdinalIgnoreCase) ||
				s.DisplayName.Contains(serviceFilter, StringComparison.OrdinalIgnoreCase) ||
				s.State.Contains(serviceFilter, StringComparison.OrdinalIgnoreCase) ||
				s.StartMode.Contains(serviceFilter, StringComparison.OrdinalIgnoreCase));
		}

		return services
			.OrderByDescending(s => string.Equals(s.State, "Running", StringComparison.OrdinalIgnoreCase))
			.ThenBy(s => s.Name)
			.ToList();
	}

	private List<NetworkConnectionSnapshot> GetVisibleNetworkConnections()
	{
		IEnumerable<NetworkConnectionSnapshot> connections = snapshot.NetworkConnections;
		if (!string.IsNullOrWhiteSpace(networkFilter))
		{
			connections = connections.Where(c =>
				c.Protocol.Contains(networkFilter, StringComparison.OrdinalIgnoreCase) ||
				c.State.Contains(networkFilter, StringComparison.OrdinalIgnoreCase) ||
				c.LocalEndpoint.Contains(networkFilter, StringComparison.OrdinalIgnoreCase) ||
				c.RemoteEndpoint.Contains(networkFilter, StringComparison.OrdinalIgnoreCase) ||
				c.ProcessName.Contains(networkFilter, StringComparison.OrdinalIgnoreCase) ||
				c.ProcessId.ToString(CultureInfo.InvariantCulture).Contains(networkFilter, StringComparison.OrdinalIgnoreCase));
		}

		return connections
			.OrderByDescending(c => string.Equals(c.State, "Established", StringComparison.OrdinalIgnoreCase))
			.ThenBy(c => c.ProcessName)
			.ThenBy(c => c.RemoteEndpoint)
			.ToList();
	}

	private List<StartupItemSnapshot> GetVisibleStartupItems()
	{
		IEnumerable<StartupItemSnapshot> items = snapshot.StartupItems;
		if (!string.IsNullOrWhiteSpace(startupFilter))
		{
			items = items.Where(i =>
				i.Name.Contains(startupFilter, StringComparison.OrdinalIgnoreCase) ||
				i.Source.Contains(startupFilter, StringComparison.OrdinalIgnoreCase) ||
				i.Location.Contains(startupFilter, StringComparison.OrdinalIgnoreCase) ||
				i.Command.Contains(startupFilter, StringComparison.OrdinalIgnoreCase));
		}

		return items.OrderBy(i => i.Source).ThenBy(i => i.Name).ToList();
	}

	private List<EventSnapshot> GetVisibleEvents()
	{
		IEnumerable<EventSnapshot> events = snapshot.Events;
		if (!string.IsNullOrWhiteSpace(eventFilter))
		{
			events = events.Where(e =>
				e.LogName.Contains(eventFilter, StringComparison.OrdinalIgnoreCase) ||
				e.Level.Contains(eventFilter, StringComparison.OrdinalIgnoreCase) ||
				e.Provider.Contains(eventFilter, StringComparison.OrdinalIgnoreCase) ||
				e.Message.Contains(eventFilter, StringComparison.OrdinalIgnoreCase) ||
				e.Id.ToString(CultureInfo.InvariantCulture).Contains(eventFilter, StringComparison.OrdinalIgnoreCase));
		}

		return events.OrderByDescending(e => e.TimeCreated ?? DateTime.MinValue).ToList();
	}

	private List<DriverSnapshot> GetVisibleDrivers()
	{
		IEnumerable<DriverSnapshot> drivers = snapshot.Drivers;
		if (!string.IsNullOrWhiteSpace(driverFilter))
		{
			drivers = drivers.Where(d =>
				d.Name.Contains(driverFilter, StringComparison.OrdinalIgnoreCase) ||
				d.DisplayName.Contains(driverFilter, StringComparison.OrdinalIgnoreCase) ||
				d.State.Contains(driverFilter, StringComparison.OrdinalIgnoreCase) ||
				d.StartMode.Contains(driverFilter, StringComparison.OrdinalIgnoreCase) ||
				(d.Path ?? "").Contains(driverFilter, StringComparison.OrdinalIgnoreCase));
		}

		return drivers
			.OrderByDescending(d => string.Equals(d.State, "Running", StringComparison.OrdinalIgnoreCase))
			.ThenBy(d => d.Name)
			.ToList();
	}

	private List<ScheduledTaskSnapshot> GetVisibleTasks()
	{
		IEnumerable<ScheduledTaskSnapshot> tasks = snapshot.ScheduledTasks;
		if (!string.IsNullOrWhiteSpace(taskFilter))
		{
			tasks = tasks.Where(t =>
				t.TaskName.Contains(taskFilter, StringComparison.OrdinalIgnoreCase) ||
				t.TaskPath.Contains(taskFilter, StringComparison.OrdinalIgnoreCase) ||
				t.State.Contains(taskFilter, StringComparison.OrdinalIgnoreCase) ||
				t.Action.Contains(taskFilter, StringComparison.OrdinalIgnoreCase));
		}

		return tasks
			.OrderByDescending(t => string.Equals(t.State, "Running", StringComparison.OrdinalIgnoreCase))
			.ThenBy(t => t.TaskPath)
			.ThenBy(t => t.TaskName)
			.ToList();
	}

	private List<FirewallRuleSnapshot> GetVisibleFirewallRules()
	{
		IEnumerable<FirewallRuleSnapshot> rules = snapshot.FirewallRules;
		if (!string.IsNullOrWhiteSpace(firewallFilter))
		{
			rules = rules.Where(r =>
				r.Name.Contains(firewallFilter, StringComparison.OrdinalIgnoreCase) ||
				r.DisplayName.Contains(firewallFilter, StringComparison.OrdinalIgnoreCase) ||
				r.Direction.Contains(firewallFilter, StringComparison.OrdinalIgnoreCase) ||
				r.Action.Contains(firewallFilter, StringComparison.OrdinalIgnoreCase) ||
				r.Profile.Contains(firewallFilter, StringComparison.OrdinalIgnoreCase) ||
				r.Group.Contains(firewallFilter, StringComparison.OrdinalIgnoreCase));
		}

		return rules
			.OrderByDescending(r => r.Enabled)
			.ThenBy(r => r.Direction)
			.ThenBy(r => r.DisplayName)
			.ToList();
	}

	private List<LocalUserSnapshot> GetVisibleUsers()
	{
		IEnumerable<LocalUserSnapshot> users = snapshot.LocalUsers;
		if (!string.IsNullOrWhiteSpace(userFilter))
		{
			users = users.Where(u =>
				u.Kind.Contains(userFilter, StringComparison.OrdinalIgnoreCase) ||
				u.Name.Contains(userFilter, StringComparison.OrdinalIgnoreCase) ||
				(u.Description ?? "").Contains(userFilter, StringComparison.OrdinalIgnoreCase) ||
				u.Members.Contains(userFilter, StringComparison.OrdinalIgnoreCase));
		}

		return users
			.OrderByDescending(u => u.IsAdmin)
			.ThenBy(u => u.Kind)
			.ThenBy(u => u.Name)
			.ToList();
	}

	private UsbDeviceSnapshot? GetSelectedUsbDevice()
	{
		var visible = GetVisibleUsbDevices();
		if (visible.Count == 0)
		{
			return null;
		}

		selectedUsbDevice = Math.Clamp(selectedUsbDevice, 0, visible.Count - 1);
		return visible[selectedUsbDevice];
	}

	private ServiceSnapshot? GetSelectedService()
	{
		var visible = GetVisibleServices();
		if (visible.Count == 0)
		{
			return null;
		}

		selectedService = Math.Clamp(selectedService, 0, visible.Count - 1);
		return visible[selectedService];
	}

	private NetworkConnectionSnapshot? GetSelectedNetworkConnection()
	{
		var visible = GetVisibleNetworkConnections();
		if (visible.Count == 0)
		{
			return null;
		}

		selectedNetworkConnection = Math.Clamp(selectedNetworkConnection, 0, visible.Count - 1);
		return visible[selectedNetworkConnection];
	}

	private StartupItemSnapshot? GetSelectedStartupItem()
	{
		var visible = GetVisibleStartupItems();
		if (visible.Count == 0)
		{
			return null;
		}

		selectedStartupItem = Math.Clamp(selectedStartupItem, 0, visible.Count - 1);
		return visible[selectedStartupItem];
	}

	private EventSnapshot? GetSelectedEvent()
	{
		var visible = GetVisibleEvents();
		if (visible.Count == 0)
		{
			return null;
		}

		selectedEvent = Math.Clamp(selectedEvent, 0, visible.Count - 1);
		return visible[selectedEvent];
	}

	private DriverSnapshot? GetSelectedDriver()
	{
		var visible = GetVisibleDrivers();
		if (visible.Count == 0)
		{
			return null;
		}

		selectedDriver = Math.Clamp(selectedDriver, 0, visible.Count - 1);
		return visible[selectedDriver];
	}

	private ScheduledTaskSnapshot? GetSelectedTask()
	{
		var visible = GetVisibleTasks();
		if (visible.Count == 0)
		{
			return null;
		}

		selectedTask = Math.Clamp(selectedTask, 0, visible.Count - 1);
		return visible[selectedTask];
	}

	private FirewallRuleSnapshot? GetSelectedFirewallRule()
	{
		var visible = GetVisibleFirewallRules();
		if (visible.Count == 0)
		{
			return null;
		}

		selectedFirewallRule = Math.Clamp(selectedFirewallRule, 0, visible.Count - 1);
		return visible[selectedFirewallRule];
	}

	private LocalUserSnapshot? GetSelectedUser()
	{
		var visible = GetVisibleUsers();
		if (visible.Count == 0)
		{
			return null;
		}

		selectedUser = Math.Clamp(selectedUser, 0, visible.Count - 1);
		return visible[selectedUser];
	}

	private List<ProcessSnapshot> GetVisibleProcesses()
	{
		IEnumerable<ProcessSnapshot> processes = snapshot.Processes;
		if (!string.IsNullOrWhiteSpace(processFilter))
		{
			processes = processes.Where(p =>
				p.Name.Contains(processFilter, StringComparison.OrdinalIgnoreCase) ||
				p.Pid.ToString().Contains(processFilter, StringComparison.OrdinalIgnoreCase));
		}

		var filtered = processes.ToList();
		if (!treeMode)
		{
			return SortProcesses(filtered).ToList();
		}

		var included = filtered.Select(p => p.Pid).ToHashSet();
		var children = filtered
			.Where(p => p.ParentPid != p.Pid && included.Contains(p.ParentPid))
			.GroupBy(p => p.ParentPid)
			.ToDictionary(group => group.Key, group => SortProcesses(group).ToList());
		var roots = SortProcesses(filtered.Where(p => !included.Contains(p.ParentPid) || p.ParentPid == p.Pid));
		var flattened = new List<ProcessSnapshot>(filtered.Count);
		var visited = new HashSet<int>();

		void AddBranch(ProcessSnapshot process)
		{
			if (!visited.Add(process.Pid))
			{
				return;
			}
			flattened.Add(process);
			if (children.TryGetValue(process.Pid, out var branch))
			{
				foreach (var child in branch)
				{
					AddBranch(child);
				}
			}
		}

		foreach (var root in roots)
		{
			AddBranch(root);
		}
		foreach (var orphan in SortProcesses(filtered.Where(p => !visited.Contains(p.Pid))))
		{
			AddBranch(orphan);
		}
		return flattened;
	}

	private IOrderedEnumerable<ProcessSnapshot> SortProcesses(IEnumerable<ProcessSnapshot> processes)
	{
		return sortColumn switch
		{
			"mem" => reverseSort ? processes.OrderByDescending(p => p.MemoryBytes) : processes.OrderBy(p => p.MemoryBytes),
			"threads" => reverseSort ? processes.OrderByDescending(p => p.ThreadCount) : processes.OrderBy(p => p.ThreadCount),
			"pid" => reverseSort ? processes.OrderByDescending(p => p.Pid) : processes.OrderBy(p => p.Pid),
			"name" => reverseSort ? processes.OrderByDescending(p => p.Name) : processes.OrderBy(p => p.Name),
			_ => reverseSort ? processes.OrderByDescending(p => p.CpuPercent) : processes.OrderBy(p => p.CpuPercent)
		};
	}

	private int GetTreeDepth(ProcessSnapshot process)
	{
		if (!treeMode)
		{
			return 0;
		}

		var lookup = snapshot.Processes.ToDictionary(p => p.Pid);
		var seen = new HashSet<int> { process.Pid };
		var depth = 0;
		var parent = process.ParentPid;
		while (depth < 8 && lookup.TryGetValue(parent, out var current) && seen.Add(parent))
		{
			depth++;
			parent = current.ParentPid;
		}
		return depth;
	}

	private ProcessSnapshot? GetSelectedProcess()
	{
		var visible = GetVisibleProcesses();
		if (visible.Count == 0)
		{
			return null;
		}

		selectedProcess = Math.Clamp(selectedProcess, 0, visible.Count - 1);
		return visible[selectedProcess];
	}

	private void ArmKillForSelectedProcess()
	{
		var process = GetSelectedProcess();
		if (process is null)
		{
			statusMessage = "No process selected";
			return;
		}

		pendingAction = new PendingAction(PendingActionKind.KillProcess, process.Value.Pid.ToString(CultureInfo.InvariantCulture), process.Value.Name);
		statusMessage = $"Press Enter to kill PID {process.Value.Pid} ({process.Value.Name}), Esc to cancel";
	}

	private void ArmPriorityChange(string priority)
	{
		var process = GetSelectedProcess();
		if (process is null)
		{
			statusMessage = "No process selected";
			return;
		}

		pendingAction = new PendingAction(PendingActionKind.SetProcessPriority, process.Value.Pid.ToString(CultureInfo.InvariantCulture), priority);
		statusMessage = $"Press Enter to set PID {process.Value.Pid} priority to {priority}, Esc to cancel";
	}

	private void ArmProcessAction(PendingActionKind kind)
	{
		var process = GetSelectedProcess();
		if (process is null)
		{
			SetStatus("No process selected", LogLevel.Warn);
			return;
		}

		pendingAction = new PendingAction(kind, process.Value.Pid.ToString(CultureInfo.InvariantCulture), process.Value.Name);
		var verb = kind switch
		{
			PendingActionKind.KillProcessTree => "kill process tree for",
			PendingActionKind.SuspendProcess => "suspend",
			PendingActionKind.ResumeProcess => "resume",
			_ => "act on"
		};
		SetStatus($"Press Enter to {verb} PID {process.Value.Pid} ({process.Value.Name}), Esc to cancel", LogLevel.Warn);
	}

	private void ArmKillForSelectedNetworkConnection()
	{
		var connection = GetSelectedNetworkConnection();
		if (connection is null || connection.Value.ProcessId <= 0)
		{
			statusMessage = "No owning process for selected connection";
			return;
		}

		pendingAction = new PendingAction(PendingActionKind.KillProcess, connection.Value.ProcessId.ToString(CultureInfo.InvariantCulture), connection.Value.ProcessName);
		statusMessage = $"Press Enter to kill network owner PID {connection.Value.ProcessId} ({connection.Value.ProcessName}), Esc to cancel";
	}

	private void ArmDeviceAction(PendingActionKind kind)
	{
		var device = GetSelectedUsbDevice();
		if (device is null)
		{
			statusMessage = "No USB device selected";
			return;
		}

		pendingAction = new PendingAction(kind, device.Value.InstanceId, device.Value.Name);
		var verb = kind == PendingActionKind.EnableDevice ? "enable" : "disable";
		statusMessage = $"Press Enter to {verb} USB device ({device.Value.Name}), Esc to cancel";
	}

	private void ArmServiceAction(PendingActionKind kind)
	{
		var service = GetSelectedService();
		if (service is null)
		{
			statusMessage = "No service selected";
			return;
		}

		pendingAction = new PendingAction(kind, service.Value.Name, service.Value.DisplayName);
		var verb = kind switch
		{
			PendingActionKind.StartService => "start",
			PendingActionKind.RestartService => "restart",
			_ => "stop"
		};
		statusMessage = $"Press Enter to {verb} service {service.Value.Name}, Esc to cancel";
	}

	private void ArmDriverAction(PendingActionKind kind)
	{
		var driver = GetSelectedDriver();
		if (driver is null)
		{
			statusMessage = "No driver selected";
			return;
		}

		pendingAction = new PendingAction(kind, driver.Value.Name, driver.Value.DisplayName);
		var verb = kind == PendingActionKind.StartService ? "start" : "stop";
		statusMessage = $"Press Enter to {verb} driver {driver.Value.Name}, Esc to cancel";
	}

	private void ArmTaskAction(PendingActionKind kind)
	{
		var task = GetSelectedTask();
		if (task is null)
		{
			SetStatus("No task selected", LogLevel.Warn);
			return;
		}

		pendingAction = new PendingAction(kind, task.Value.Id, task.Value.TaskName);
		var verb = kind switch
		{
			PendingActionKind.EnableTask => "enable",
			PendingActionKind.DisableTask => "disable",
			_ => "start"
		};
		SetStatus($"Press Enter to {verb} task {task.Value.TaskPath}{task.Value.TaskName}, Esc to cancel", LogLevel.Warn);
	}

	private void ArmFirewallAction(PendingActionKind kind)
	{
		var rule = GetSelectedFirewallRule();
		if (rule is null)
		{
			SetStatus("No firewall rule selected", LogLevel.Warn);
			return;
		}

		pendingAction = new PendingAction(kind, rule.Value.Name, rule.Value.DisplayName);
		var verb = kind == PendingActionKind.EnableFirewallRule ? "enable" : "disable";
		SetStatus($"Press Enter to {verb} firewall rule {rule.Value.DisplayName}, Esc to cancel", LogLevel.Warn);
	}

	private void ExecutePendingAction()
	{
		if (pendingAction is null)
		{
			return;
		}

		var action = pendingAction.Value;
		pendingAction = null;
		try
		{
			switch (action.Kind)
			{
				case PendingActionKind.KillProcess:
					using (var process = Process.GetProcessById(int.Parse(action.Id, CultureInfo.InvariantCulture)))
					{
						process.Kill(entireProcessTree: false);
					}
					SetStatus($"Kill signal sent to PID {action.Id}", LogLevel.Warn);
					break;
				case PendingActionKind.KillProcessTree:
					WindowsActions.KillProcessTree(int.Parse(action.Id, CultureInfo.InvariantCulture));
					SetStatus($"Kill tree signal sent to PID {action.Id}", LogLevel.Warn);
					break;
				case PendingActionKind.SuspendProcess:
					WindowsActions.SuspendProcess(int.Parse(action.Id, CultureInfo.InvariantCulture));
					SetStatus($"Suspended PID {action.Id}", LogLevel.Warn);
					break;
				case PendingActionKind.ResumeProcess:
					WindowsActions.ResumeProcess(int.Parse(action.Id, CultureInfo.InvariantCulture));
					SetStatus($"Resumed PID {action.Id}");
					break;
				case PendingActionKind.SetProcessPriority:
					WindowsActions.SetProcessPriority(int.Parse(action.Id, CultureInfo.InvariantCulture), action.Name);
					SetStatus($"Priority for PID {action.Id} set to {action.Name}");
					break;
				case PendingActionKind.DisableDevice:
					WindowsActions.DisablePnpDevice(action.Id);
					SetStatus($"Disable requested for USB device", LogLevel.Warn);
					sampler.RefreshInventory();
					break;
				case PendingActionKind.EnableDevice:
					WindowsActions.EnablePnpDevice(action.Id);
					SetStatus($"Enable requested for USB device");
					sampler.RefreshInventory();
					break;
				case PendingActionKind.StopService:
					WindowsActions.StopService(action.Id);
					SetStatus($"Stop requested for service {action.Id}", LogLevel.Warn);
					sampler.RefreshInventory();
					break;
				case PendingActionKind.RestartService:
					WindowsActions.RestartService(action.Id);
					SetStatus($"Restart requested for service {action.Id}", LogLevel.Warn);
					sampler.RefreshInventory();
					break;
				case PendingActionKind.StartService:
					WindowsActions.StartService(action.Id);
					SetStatus($"Start requested for service {action.Id}");
					sampler.RefreshInventory();
					break;
				case PendingActionKind.EnableTask:
					WindowsActions.EnableScheduledTask(action.Id);
					SetStatus($"Enabled task {action.Name}");
					sampler.RefreshInventory();
					break;
				case PendingActionKind.DisableTask:
					WindowsActions.DisableScheduledTask(action.Id);
					SetStatus($"Disabled task {action.Name}", LogLevel.Warn);
					sampler.RefreshInventory();
					break;
				case PendingActionKind.StartTask:
					WindowsActions.StartScheduledTask(action.Id);
					SetStatus($"Started task {action.Name}");
					sampler.RefreshInventory();
					break;
				case PendingActionKind.EnableFirewallRule:
					WindowsActions.SetFirewallRule(action.Id, enabled: true);
					SetStatus($"Enabled firewall rule {action.Name}");
					sampler.RefreshInventory();
					break;
				case PendingActionKind.DisableFirewallRule:
					WindowsActions.SetFirewallRule(action.Id, enabled: false);
					SetStatus($"Disabled firewall rule {action.Name}", LogLevel.Warn);
					sampler.RefreshInventory();
					break;
			}
			nextSample = DateTime.MinValue;
		}
		catch (Exception ex) when (ex is ArgumentException or InvalidOperationException or System.ComponentModel.Win32Exception)
		{
			SetStatus($"Action failed: {ex.Message}", LogLevel.Error);
		}
	}

	private void Draw()
	{
		var width = Math.Max(1, Console.WindowWidth);
		var height = Math.Max(1, Console.WindowHeight);
		var canvas = new Canvas(width, height);

		if (width < 80 || height < 24)
		{
			canvas.Text(1, 1, Theme.Error, "winbtop needs at least 80x24 cells.");
			canvas.Text(2, 1, Theme.Muted, $"Current: {width}x{height}. Press q to quit.");
			Terminal.Write(canvas.Render());
			return;
		}

		if (DateTime.UtcNow < splashUntil)
		{
			DrawSplash(canvas, width, height);
			Terminal.Write(canvas.Render());
			return;
		}

		var now = DateTime.Now;
		var title = " MandyTop v0.7 ";
		var status = paused ? "PAUSED" : $"{intervalMs} ms";
		var mode = filtering ? $"FILTER:{GetCurrentFilter()}_" : pendingAction is not null ? "CONFIRM ACTION" : status;
		var health = ComputeHealthScore();
		var banner = $"{title} health {health.Score:0}% {health.Label}  {now:yyyy-MM-dd HH:mm:ss}  {mode}  1 mon 2 usb 3 svc 4 net 5 start 6 events 7 drv 8 sec 9 tasks F9 fw F10 users";
		canvas.Text(1, 1, Theme.Title, Text.Fit(banner, width));
		if (!string.IsNullOrWhiteSpace(statusMessage))
		{
			canvas.Text(1, Math.Max(1, width - Math.Min(width - 1, statusMessage.Length + 2)), pendingAction is null ? Theme.Highlight : Theme.Warn, Text.Fit(statusMessage, Math.Min(width - 2, statusMessage.Length)));
		}

		var logHeight = Math.Clamp(height / 6, 4, 6);
		var contentHeight = height - logHeight;
		if (viewMode == ViewMode.Usb)
		{
			DrawUsbPage(canvas, 1, 2, width, contentHeight - 1);
		}
		else if (viewMode == ViewMode.Services)
		{
			DrawServicesPage(canvas, 1, 2, width, contentHeight - 1);
		}
		else if (viewMode == ViewMode.Network)
		{
			DrawNetworkPage(canvas, 1, 2, width, contentHeight - 1);
		}
		else if (viewMode == ViewMode.Startup)
		{
			DrawStartupPage(canvas, 1, 2, width, contentHeight - 1);
		}
		else if (viewMode == ViewMode.Events)
		{
			DrawEventsPage(canvas, 1, 2, width, contentHeight - 1);
		}
		else if (viewMode == ViewMode.Drivers)
		{
			DrawDriversPage(canvas, 1, 2, width, contentHeight - 1);
		}
		else if (viewMode == ViewMode.Security)
		{
			DrawSecurityPage(canvas, 1, 2, width, contentHeight - 1);
		}
		else if (viewMode == ViewMode.Tasks)
		{
			DrawTasksPage(canvas, 1, 2, width, contentHeight - 1);
		}
		else if (viewMode == ViewMode.Firewall)
		{
			DrawFirewallPage(canvas, 1, 2, width, contentHeight - 1);
		}
		else if (viewMode == ViewMode.Users)
		{
			DrawUsersPage(canvas, 1, 2, width, contentHeight - 1);
		}
		else
		{
			var cpuHeight = Math.Clamp(contentHeight / 4, 7, 11);
			var lowerTop = cpuHeight + 2;
			var lowerHeight = Math.Clamp(contentHeight / 4, 7, 11);
			var procTop = lowerTop + lowerHeight + 1;
			var procHeight = contentHeight - procTop + 1;
			var leftWidth = width / 2;
			var rightWidth = width - leftWidth;

			DrawCpu(canvas, 1, 2, width, cpuHeight);
			DrawMemNet(canvas, 1, lowerTop, leftWidth, lowerHeight);
			DrawSystemDisks(canvas, leftWidth + 1, lowerTop, rightWidth, lowerHeight);
			DrawProcesses(canvas, 1, procTop, width, procHeight);
		}

		DrawLiveLog(canvas, 1, contentHeight + 1, width, logHeight);

		if (showHelp)
		{
			DrawHelp(canvas, width, height);
		}

		Terminal.Write(canvas.Render());
	}

	private void DrawSplash(Canvas canvas, int width, int height)
	{
		var art = new[]
		{
			" __  __                 _       _____          ",
			"|  \\/  | __ _ _ __   __| |_   _|_   _|__  _ __ ",
			"| |\\/| |/ _` | '_ \\ / _` | | | | | |/ _ \\| '_ \\",
			"| |  | | (_| | | | | (_| | |_| | | | (_) | |_) |",
			"|_|  |_|\\__,_|_| |_|\\__,_|\\__, | |_|\\___/| .__/",
			"                          |___/          |_|   "
		};
		var startY = Math.Max(2, (height - art.Length) / 2 - 2);
		var startX = Math.Max(1, (width - art[0].Length) / 2 + 1);
		var colors = new[] { Theme.TransBlue, Theme.TransPink, Theme.TransWhite, Theme.TransPink, Theme.TransBlue, Theme.TransWhite };
		for (var i = 0; i < art.Length; i++)
		{
			canvas.Text(startY + i, startX, colors[i], art[i]);
		}
		canvas.Text(startY + art.Length + 2, Math.Max(1, (width - 36) / 2), Theme.Highlight, "Windows Terminal system cockpit v0.7");
		canvas.Text(startY + art.Length + 3, Math.Max(1, (width - 30) / 2), Theme.SoftText, "trans colors, admin powers, logs");
	}

	private HealthScore ComputeHealthScore()
	{
		var score = 100;
		score -= (int)Math.Clamp((snapshot.Cpu.TotalPercent - 75) * 0.7, 0, 20);
		score -= (int)Math.Clamp((snapshot.Memory.UsedPercent - 80) * 0.8, 0, 18);
		var worstDisk = snapshot.Disks.Count == 0 ? 0 : snapshot.Disks.Max(d => d.UsedPercent);
		score -= (int)Math.Clamp((worstDisk - 88) * 1.1, 0, 15);
		var errorCount = snapshot.Events.Count(e => e.Level.Contains("Error", StringComparison.OrdinalIgnoreCase));
		var warningCount = snapshot.Events.Count(e => e.Level.Contains("Warning", StringComparison.OrdinalIgnoreCase));
		score -= Math.Min(18, errorCount * 3 + warningCount);
		if (!string.Equals(snapshot.Security.DefenderRealTime, "True", StringComparison.OrdinalIgnoreCase))
		{
			score -= 14;
		}
		if (!string.Equals(snapshot.Security.DefenderAntivirus, "True", StringComparison.OrdinalIgnoreCase))
		{
			score -= 16;
		}
		foreach (var value in new[] { snapshot.Security.FirewallDomain, snapshot.Security.FirewallPrivate, snapshot.Security.FirewallPublic })
		{
			if (!string.Equals(value, "True", StringComparison.OrdinalIgnoreCase))
			{
				score -= 8;
			}
		}
		if (snapshot.Security.PendingReboot)
		{
			score -= 8;
		}

		score = Math.Clamp(score, 0, 100);
		var label = score switch
		{
			>= 90 => "excellent",
			>= 75 => "good",
			>= 55 => "watch",
			>= 35 => "rough",
			_ => "critical"
		};
		return new HealthScore(score, label);
	}

	private void DrawLiveLog(Canvas canvas, int x, int y, int w, int h)
	{
		canvas.Box(x, y, w, h, "live log");
		var rows = Math.Max(0, h - 2);
		var recent = logEntries.TakeLast(rows).ToList();
		for (var i = 0; i < recent.Count; i++)
		{
			var entry = recent[i];
			var color = entry.Level switch
			{
				LogLevel.Error => Theme.Error,
				LogLevel.Warn => Theme.Warn,
				_ => Theme.SoftText
			};
			canvas.Text(y + 1 + i, x + 2, color, Text.Fit($"{entry.Time:HH:mm:ss} [{entry.Level}] {entry.Message}", Math.Max(5, w - 4)));
		}
	}

	private void DrawCpu(Canvas canvas, int x, int y, int w, int h)
	{
		canvas.Box(x, y, w, h, "cpu");
		canvas.Text(y + 1, x + 2, Theme.Highlight, $"Total {snapshot.Cpu.TotalPercent,5:0.0}%");
		canvas.Text(y + 1, x + 17, Theme.Muted, $"Logical CPUs: {snapshot.Cpu.CorePercents.Length}");
		canvas.Text(y + 2, x + 2, Theme.Cpu, Graph.Line(snapshot.Cpu.History, Math.Max(1, w - 4), 100));
		DrawMandyTopArt(canvas, x, y, w, h);

		var cols = Math.Max(1, Math.Min(snapshot.Cpu.CorePercents.Length, (h - 4) * 2));
		var rows = (int)Math.Ceiling(cols / 2.0);
		for (var i = 0; i < cols; i++)
		{
			var row = y + 4 + (i / 2);
			var col = x + 2 + (i % 2) * Math.Max(25, (w - 4) / 2);
			if (row >= y + h - 1 || col >= x + w - 4)
			{
				break;
			}

			var value = snapshot.Cpu.CorePercents[i];
			var meterWidth = Math.Max(5, Math.Min(12, x + w - col - 11));
			canvas.Text(row, col, Theme.Muted, $"cpu{i,2}");
			canvas.Text(row, col + 6, Color.Percent(value), Meter(value, meterWidth));
			canvas.Text(row, col + 8 + meterWidth, Theme.SoftText, $"{value,5:0}%");
		}
	}

	private static void DrawMandyTopArt(Canvas canvas, int x, int y, int w, int h)
	{
		if (w < 104 || h < 9)
		{
			return;
		}

		var art = new[]
		{
			" __  __                 _       _____          ",
			"|  \\/  | __ _ _ __   __| |_   _|_   _|__  _ __ ",
			"| |\\/| |/ _` | '_ \\ / _` | | | | | |/ _ \\| '_ \\",
			"| |  | | (_| | | | | (_| | |_| | | | (_) | |_) |",
			"|_|  |_|\\__,_|_| |_|\\__,_|\\__, | |_|\\___/| .__/",
			"                          |___/          |_|   "
		};
		var colors = new[] { Theme.TransBlue, Theme.TransPink, Theme.TransWhite, Theme.TransPink, Theme.TransBlue, Theme.TransWhite };
		var startX = x + w - art[0].Length - 3;
		var startY = y + 2;
		for (var i = 0; i < art.Length; i++)
		{
			canvas.Text(startY + i, startX, colors[i], art[i]);
		}
	}

	private void DrawMemNet(Canvas canvas, int x, int y, int w, int h)
	{
		canvas.Box(x, y, w, h, "mem / net");
		var mem = snapshot.Memory;
		var usedGb = mem.UsedBytes / Units.Gib;
		var totalGb = mem.TotalBytes / Units.Gib;
		canvas.Text(y + 1, x + 2, Theme.Highlight, $"RAM {usedGb:0.0}/{totalGb:0.0} GiB");
		canvas.Text(y + 1, x + Math.Min(w - 16, 24), Color.Percent(mem.UsedPercent), Meter(mem.UsedPercent, Math.Max(8, w - 32)));
		canvas.Text(y + 2, x + 2, Theme.Memory, Graph.Line(mem.History, Math.Max(1, w - 4), 100));

		var net = snapshot.Network;
		canvas.Text(y + 4, x + 2, Theme.Highlight, $"NET down {Units.Rate(net.DownloadBytesPerSec),12}  up {Units.Rate(net.UploadBytesPerSec),12}");
		canvas.Text(y + 5, x + 2, Theme.Net, Graph.Line(net.History, Math.Max(1, w - 4), Math.Max(1, net.HistoryMax)));

		var line = y + 6;
		if (line < y + h - 1)
		{
			canvas.Text(line++, x + 2, Theme.SoftText, $"Session down {Units.Size(net.TotalDownloadedBytes)}  up {Units.Size(net.TotalUploadedBytes)}");
		}
		foreach (var adapter in net.Adapters.Take(Math.Max(0, h - 8)))
		{
			canvas.Text(line++, x + 2, Theme.Muted, $"{Text.Fit(adapter.Name, 20),-20} {Units.Rate(adapter.DownloadBytesPerSec),10} {Units.Rate(adapter.UploadBytesPerSec),10}");
		}
	}

	private void DrawSystemDisks(Canvas canvas, int x, int y, int w, int h)
	{
		canvas.Box(x, y, w, h, "system / disks");
		var system = snapshot.System;
		var battery = system.BatteryPercent is null ? "battery n/a" : $"battery {system.BatteryPercent:0}%";
		var power = system.OnAcPower is null ? "" : system.OnAcPower.Value ? " AC" : " battery";
		canvas.Text(y + 1, x + 2, Theme.Highlight, Text.Fit(system.MachineName, Math.Max(8, w / 3)));
		canvas.Text(y + 1, x + Math.Max(15, w / 3 + 1), Theme.Muted, Text.Fit(system.OsDescription, Math.Max(10, w - w / 3 - 4)));
		canvas.Text(y + 2, x + 2, Theme.Muted, $"Uptime {Units.Duration(system.Uptime)}");
		canvas.Text(y + 2, x + Math.Max(22, w / 2), Color.Percent(100 - (system.BatteryPercent ?? 100)), Text.Fit(battery + power, Math.Max(8, w / 2 - 3)));
		if (w >= 62)
		{
			canvas.Text(y + 3, x + 2, Theme.SoftText, $"RAM free {Units.Size(snapshot.Memory.AvailableBytes)}  Commit {Units.Size(snapshot.Memory.PageFileUsedBytes)}/{Units.Size(snapshot.Memory.PageFileTotalBytes)}");
		}

		var row = y + 4;
		canvas.Text(row++, x + 2, Theme.Title, " DRIVE      USED        FREE        TOTAL");
		foreach (var disk in snapshot.Disks.Take(Math.Max(0, h - 6)))
		{
			var name = Text.Fit($"{disk.Name} {disk.Format}", 10);
			canvas.Text(row, x + 2, Theme.Muted, $"{name,-10} {Units.Size(disk.UsedBytes),10} {Units.Size(disk.FreeBytes),10} {Units.Size(disk.TotalBytes),10}");
			canvas.Text(row, x + w - 13, Color.Percent(disk.UsedPercent), $"{disk.UsedPercent,5:0}%");
			row++;
		}
	}

	private void DrawUsbPage(Canvas canvas, int x, int y, int w, int h)
	{
		var filterText = string.IsNullOrWhiteSpace(usbFilter) ? "" : $" filter:{usbFilter}";
		canvas.Box(x, y, w, h, $"usb devices / installed drivers{filterText}");
		var devices = GetVisibleUsbDevices();
		selectedUsbDevice = Math.Clamp(selectedUsbDevice, 0, Math.Max(0, devices.Count - 1));

		var driverHeight = Math.Clamp(h / 3, 7, 12);
		var deviceHeight = h - driverHeight;
		var detailRows = deviceHeight >= 14 ? 4 : 0;
		var visibleRows = Math.Max(0, deviceHeight - 4 - detailRows);
		if (selectedUsbDevice < usbScrollOffset)
		{
			usbScrollOffset = selectedUsbDevice;
		}
		else if (selectedUsbDevice >= usbScrollOffset + visibleRows)
		{
			usbScrollOffset = selectedUsbDevice - visibleRows + 1;
		}

		var connected = snapshot.UsbDevices.Count(d => d.Present);
		canvas.Text(y + 1, x + 2, Theme.Highlight, $"Connected {connected}  Installed USB devices {snapshot.UsbDevices.Count}  Drivers {snapshot.UsbDrivers.Count}");
		canvas.Text(y + 2, x + 2, Theme.Title, "  PRES  STATUS       CLASS        NAME");
		for (var i = 0; i < visibleRows && i + usbScrollOffset < devices.Count; i++)
		{
			var device = devices[i + usbScrollOffset];
			var row = y + 3 + i;
			var selected = i + usbScrollOffset == selectedUsbDevice;
			var color = selected ? Theme.Selected : device.Present ? Theme.Muted : Theme.SoftText;
			var prefix = selected ? ">" : " ";
			var present = device.Present ? "yes" : "no ";
			var statusColor = string.Equals(device.Status, "OK", StringComparison.OrdinalIgnoreCase) ? color : Theme.Warn;
			canvas.Text(row, x + 2, color, $"{prefix} {present,-4} ");
			canvas.Text(row, x + 10, statusColor, $"{Text.Fit(device.Status, 10),-10}");
			canvas.Text(row, x + 22, color, $"{Text.Fit(device.Class, 11),-11} {Text.Fit(device.Name, Math.Max(8, w - 37))}");
		}

		if (detailRows > 0 && devices.Count > 0)
		{
			var device = devices[selectedUsbDevice];
			var top = y + deviceHeight - detailRows;
			canvas.Text(top, x + 2, Theme.Border, new string('-', Math.Max(1, w - 4)));
			canvas.Text(top + 1, x + 2, Theme.Highlight, Text.Fit($"Selected USB: {device.Name}", Math.Max(5, w - 4)));
			canvas.Text(top + 2, x + 2, Theme.Muted, Text.Fit($"Status {device.Status}  Class {device.Class}  InstanceId: {device.InstanceId}", Math.Max(5, w - 4)));
			var devicePending = pendingAction?.Id == device.InstanceId;
			canvas.Text(top + 3, x + 2, devicePending ? Theme.Warn : Theme.SoftText, devicePending ? "Enter confirms action, Esc cancels" : "d/x/Delete disable device  a enable device  c copy InstanceId  e export CSV  r refresh");
		}
		else if (devices.Count == 0)
		{
			canvas.Text(y + 3, x + 2, Theme.Warn, "No USB devices match the current filter.");
		}

		var driverTop = y + deviceHeight;
		canvas.Text(driverTop, x + 2, Theme.Border, new string('-', Math.Max(1, w - 4)));
		canvas.Text(driverTop + 1, x + 2, Theme.Title, " INSTALLED USB DRIVERS");
		canvas.Text(driverTop + 2, x + 2, Theme.Title, " INF             PROVIDER           VERSION         DEVICE");
		var rowDriver = driverTop + 3;
		foreach (var driver in snapshot.UsbDrivers.Take(Math.Max(0, h - deviceHeight - 4)))
		{
			var line = $"{Text.Fit(driver.InfName, 15),-15} {Text.Fit(driver.Provider, 18),-18} {Text.Fit(driver.Version, 14),-14} {Text.Fit(driver.DeviceName, Math.Max(8, w - 57))}";
			canvas.Text(rowDriver++, x + 2, Theme.Muted, Text.Fit(line, w - 4));
		}
	}

	private void DrawServicesPage(Canvas canvas, int x, int y, int w, int h)
	{
		var filterText = string.IsNullOrWhiteSpace(serviceFilter) ? "" : $" filter:{serviceFilter}";
		canvas.Box(x, y, w, h, $"services{filterText}");
		var services = GetVisibleServices();
		selectedService = Math.Clamp(selectedService, 0, Math.Max(0, services.Count - 1));
		var running = snapshot.Services.Count(s => string.Equals(s.State, "Running", StringComparison.OrdinalIgnoreCase));
		var detailRows = h >= 14 ? 4 : 0;
		var visibleRows = Math.Max(0, h - 4 - detailRows);

		if (selectedService < serviceScrollOffset)
		{
			serviceScrollOffset = selectedService;
		}
		else if (selectedService >= serviceScrollOffset + visibleRows)
		{
			serviceScrollOffset = selectedService - visibleRows + 1;
		}

		canvas.Text(y + 1, x + 2, Theme.Highlight, $"Services {snapshot.Services.Count}  Running {running}");
		canvas.Text(y + 2, x + 2, Theme.Title, "  STATE       START       PID      NAME                 DISPLAY");
		for (var i = 0; i < visibleRows && i + serviceScrollOffset < services.Count; i++)
		{
			var service = services[i + serviceScrollOffset];
			var row = y + 3 + i;
			var selected = i + serviceScrollOffset == selectedService;
			var runningService = string.Equals(service.State, "Running", StringComparison.OrdinalIgnoreCase);
			var color = selected ? Theme.Selected : runningService ? Theme.Muted : Theme.SoftText;
			var prefix = selected ? ">" : " ";
			var stateColor = runningService ? color : Theme.Warn;
			canvas.Text(row, x + 2, color, prefix);
			canvas.Text(row, x + 4, stateColor, $"{Text.Fit(service.State, 10),-10}");
			var line = $" {Text.Fit(service.StartMode, 10),-10} {service.ProcessId,7}  {Text.Fit(service.Name, 20),-20} {Text.Fit(service.DisplayName, Math.Max(8, w - 60))}";
			canvas.Text(row, x + 14, color, Text.Fit(line, Math.Max(5, w - 16)));
		}

		if (detailRows > 0 && services.Count > 0)
		{
			var service = services[selectedService];
			var top = y + h - detailRows;
			canvas.Text(top, x + 2, Theme.Border, new string('-', Math.Max(1, w - 4)));
			canvas.Text(top + 1, x + 2, Theme.Highlight, Text.Fit($"Selected service: {service.Name}  ({service.DisplayName})", Math.Max(5, w - 4)));
			canvas.Text(top + 2, x + 2, Theme.Muted, Text.Fit($"State {service.State}  Start {service.StartMode}  PID {service.ProcessId}  Depends: {Blank(service.DependsOn)}  Used by: {Blank(service.DependedOnBy)}", Math.Max(5, w - 4)));
			var servicePending = pendingAction?.Id == service.Name;
			canvas.Text(top + 3, x + 2, servicePending ? Theme.Warn : Theme.SoftText, servicePending ? "Enter confirms action, Esc cancels" : "o/x/Delete stop  r restart  a start  c copy name  e export CSV  Tab refresh");
		}
		else if (services.Count == 0)
		{
			canvas.Text(y + 3, x + 2, Theme.Warn, "No services match the current filter.");
		}
	}

	private void DrawNetworkPage(Canvas canvas, int x, int y, int w, int h)
	{
		var filterText = string.IsNullOrWhiteSpace(networkFilter) ? "" : $" filter:{networkFilter}";
		canvas.Box(x, y, w, h, $"network connections{filterText}");
		var connections = GetVisibleNetworkConnections();
		selectedNetworkConnection = Math.Clamp(selectedNetworkConnection, 0, Math.Max(0, connections.Count - 1));
		var established = snapshot.NetworkConnections.Count(c => string.Equals(c.State, "Established", StringComparison.OrdinalIgnoreCase));
		var listening = snapshot.NetworkConnections.Count(c => string.Equals(c.State, "Listen", StringComparison.OrdinalIgnoreCase));
		var detailRows = h >= 14 ? 4 : 0;
		var visibleRows = Math.Max(0, h - 4 - detailRows);
		KeepSelectionVisible(selectedNetworkConnection, visibleRows, ref networkScrollOffset);

		canvas.Text(y + 1, x + 2, Theme.Highlight, $"TCP/UDP {snapshot.NetworkConnections.Count}  Established {established}  Listening {listening}");
		canvas.Text(y + 2, x + 2, Theme.Title, "  PROTO  STATE         LOCAL                         REMOTE                        PID   PROCESS");
		for (var i = 0; i < visibleRows && i + networkScrollOffset < connections.Count; i++)
		{
			var connection = connections[i + networkScrollOffset];
			var row = y + 3 + i;
			var selected = i + networkScrollOffset == selectedNetworkConnection;
			var color = selected ? Theme.Selected : string.Equals(connection.State, "Established", StringComparison.OrdinalIgnoreCase) ? Theme.Muted : Theme.SoftText;
			var line = $"{(selected ? ">" : " ")} {connection.Protocol,-5} {Text.Fit(connection.State, 12),-12} {Text.Fit(connection.LocalEndpoint, 29),-29} {Text.Fit(connection.RemoteEndpoint, 29),-29} {connection.ProcessId,5} {Text.Fit(connection.ProcessName, Math.Max(8, w - 91))}";
			canvas.Text(row, x + 2, color, Text.Fit(line, w - 4));
		}

		if (detailRows > 0 && connections.Count > 0)
		{
			var connection = connections[selectedNetworkConnection];
			var top = y + h - detailRows;
			canvas.Text(top, x + 2, Theme.Border, new string('-', Math.Max(1, w - 4)));
			canvas.Text(top + 1, x + 2, Theme.Highlight, Text.Fit($"Selected connection: {connection.Protocol} {connection.LocalEndpoint} -> {connection.RemoteEndpoint}", Math.Max(5, w - 4)));
			canvas.Text(top + 2, x + 2, Theme.Muted, Text.Fit($"State {connection.State}  PID {connection.ProcessId}  Process {connection.ProcessName}", Math.Max(5, w - 4)));
			canvas.Text(top + 3, x + 2, pendingAction?.Id == connection.ProcessId.ToString(CultureInfo.InvariantCulture) ? Theme.Warn : Theme.SoftText, pendingAction?.Id == connection.ProcessId.ToString(CultureInfo.InvariantCulture) ? "Enter confirms kill, Esc cancels" : "x/Delete kill owning process  c copy endpoint  e export CSV  r refresh");
		}
		else if (connections.Count == 0)
		{
			canvas.Text(y + 3, x + 2, Theme.Warn, "No network connections match the current filter.");
		}
	}

	private void DrawStartupPage(Canvas canvas, int x, int y, int w, int h)
	{
		var filterText = string.IsNullOrWhiteSpace(startupFilter) ? "" : $" filter:{startupFilter}";
		canvas.Box(x, y, w, h, $"startup entries{filterText}");
		var items = GetVisibleStartupItems();
		selectedStartupItem = Math.Clamp(selectedStartupItem, 0, Math.Max(0, items.Count - 1));
		var detailRows = h >= 14 ? 4 : 0;
		var visibleRows = Math.Max(0, h - 4 - detailRows);
		KeepSelectionVisible(selectedStartupItem, visibleRows, ref startupScrollOffset);

		canvas.Text(y + 1, x + 2, Theme.Highlight, $"Startup entries {snapshot.StartupItems.Count}");
		canvas.Text(y + 2, x + 2, Theme.Title, "  SOURCE        NAME                         LOCATION / COMMAND");
		for (var i = 0; i < visibleRows && i + startupScrollOffset < items.Count; i++)
		{
			var item = items[i + startupScrollOffset];
			var row = y + 3 + i;
			var selected = i + startupScrollOffset == selectedStartupItem;
			var color = selected ? Theme.Selected : Theme.Muted;
			var line = $"{(selected ? ">" : " ")} {Text.Fit(item.Source, 12),-12} {Text.Fit(item.Name, 28),-28} {Text.Fit(item.Location + " :: " + item.Command, Math.Max(8, w - 50))}";
			canvas.Text(row, x + 2, color, Text.Fit(line, w - 4));
		}

		if (detailRows > 0 && items.Count > 0)
		{
			var item = items[selectedStartupItem];
			var top = y + h - detailRows;
			canvas.Text(top, x + 2, Theme.Border, new string('-', Math.Max(1, w - 4)));
			canvas.Text(top + 1, x + 2, Theme.Highlight, Text.Fit($"Selected startup: {item.Name}  Source {item.Source}", Math.Max(5, w - 4)));
			canvas.Text(top + 2, x + 2, Theme.Muted, Text.Fit($"Location {item.Location}  Command: {item.Command}", Math.Max(5, w - 4)));
			canvas.Text(top + 3, x + 2, Theme.SoftText, "c copy command  e export CSV  r refresh");
		}
		else if (items.Count == 0)
		{
			canvas.Text(y + 3, x + 2, Theme.Warn, "No startup entries match the current filter.");
		}
	}

	private void DrawEventsPage(Canvas canvas, int x, int y, int w, int h)
	{
		var filterText = string.IsNullOrWhiteSpace(eventFilter) ? "" : $" filter:{eventFilter}";
		canvas.Box(x, y, w, h, $"recent errors / warnings{filterText}");
		var events = GetVisibleEvents();
		selectedEvent = Math.Clamp(selectedEvent, 0, Math.Max(0, events.Count - 1));
		var errors = snapshot.Events.Count(e => e.Level.Contains("Error", StringComparison.OrdinalIgnoreCase));
		var warnings = snapshot.Events.Count(e => e.Level.Contains("Warning", StringComparison.OrdinalIgnoreCase));
		var detailRows = h >= 14 ? 5 : 0;
		var visibleRows = Math.Max(0, h - 4 - detailRows);
		KeepSelectionVisible(selectedEvent, visibleRows, ref eventScrollOffset);

		canvas.Text(y + 1, x + 2, Theme.Highlight, $"Last 24h sample {snapshot.Events.Count}  Errors {errors}  Warnings {warnings}");
		canvas.Text(y + 2, x + 2, Theme.Title, "  TIME                LOG          LVL       ID     PROVIDER / MESSAGE");
		for (var i = 0; i < visibleRows && i + eventScrollOffset < events.Count; i++)
		{
			var item = events[i + eventScrollOffset];
			var row = y + 3 + i;
			var selected = i + eventScrollOffset == selectedEvent;
			var color = selected ? Theme.Selected : item.Level.Contains("Error", StringComparison.OrdinalIgnoreCase) ? Theme.Error : Theme.Warn;
			var time = item.TimeCreated?.ToString("MM-dd HH:mm:ss") ?? "n/a";
			var line = $"{(selected ? ">" : " ")} {time,-16} {Text.Fit(item.LogName, 12),-12} {Text.Fit(item.Level, 9),-9} {item.Id,6} {Text.Fit(item.Provider + ": " + item.Message, Math.Max(8, w - 61))}";
			canvas.Text(row, x + 2, color, Text.Fit(line, w - 4));
		}

		if (detailRows > 0 && events.Count > 0)
		{
			var item = events[selectedEvent];
			var top = y + h - detailRows;
			canvas.Text(top, x + 2, Theme.Border, new string('-', Math.Max(1, w - 4)));
			canvas.Text(top + 1, x + 2, Theme.Highlight, Text.Fit($"Event {item.Id}  {item.Level}  {item.LogName}  {item.TimeCreated}", Math.Max(5, w - 4)));
			canvas.Text(top + 2, x + 2, Theme.Muted, Text.Fit($"Provider {item.Provider}", Math.Max(5, w - 4)));
			canvas.Text(top + 3, x + 2, Theme.SoftText, Text.Fit(item.Message, Math.Max(5, w - 4)));
			canvas.Text(top + 4, x + 2, Theme.SoftText, "c copy event  e export CSV  r refresh");
		}
		else if (events.Count == 0)
		{
			canvas.Text(y + 3, x + 2, Theme.Warn, "No recent errors or warnings match the current filter.");
		}
	}

	private void DrawDriversPage(Canvas canvas, int x, int y, int w, int h)
	{
		var filterText = string.IsNullOrWhiteSpace(driverFilter) ? "" : $" filter:{driverFilter}";
		canvas.Box(x, y, w, h, $"system drivers{filterText}");
		var drivers = GetVisibleDrivers();
		selectedDriver = Math.Clamp(selectedDriver, 0, Math.Max(0, drivers.Count - 1));
		var running = snapshot.Drivers.Count(d => string.Equals(d.State, "Running", StringComparison.OrdinalIgnoreCase));
		var detailRows = h >= 14 ? 4 : 0;
		var visibleRows = Math.Max(0, h - 4 - detailRows);
		KeepSelectionVisible(selectedDriver, visibleRows, ref driverScrollOffset);

		canvas.Text(y + 1, x + 2, Theme.Highlight, $"Drivers {snapshot.Drivers.Count}  Running {running}");
		canvas.Text(y + 2, x + 2, Theme.Title, "  STATE       START       NAME                         DISPLAY");
		for (var i = 0; i < visibleRows && i + driverScrollOffset < drivers.Count; i++)
		{
			var driver = drivers[i + driverScrollOffset];
			var row = y + 3 + i;
			var selected = i + driverScrollOffset == selectedDriver;
			var color = selected ? Theme.Selected : string.Equals(driver.State, "Running", StringComparison.OrdinalIgnoreCase) ? Theme.Muted : Theme.SoftText;
			var line = $"{(selected ? ">" : " ")} {Text.Fit(driver.State, 10),-10} {Text.Fit(driver.StartMode, 10),-10} {Text.Fit(driver.Name, 28),-28} {Text.Fit(driver.DisplayName, Math.Max(8, w - 56))}";
			canvas.Text(row, x + 2, color, Text.Fit(line, w - 4));
		}

		if (detailRows > 0 && drivers.Count > 0)
		{
			var driver = drivers[selectedDriver];
			var top = y + h - detailRows;
			canvas.Text(top, x + 2, Theme.Border, new string('-', Math.Max(1, w - 4)));
			canvas.Text(top + 1, x + 2, Theme.Highlight, Text.Fit($"Selected driver: {driver.Name}  ({driver.DisplayName})", Math.Max(5, w - 4)));
			canvas.Text(top + 2, x + 2, Theme.Muted, Text.Fit($"State {driver.State}  Start {driver.StartMode}  Path: {driver.Path ?? "n/a"}", Math.Max(5, w - 4)));
			canvas.Text(top + 3, x + 2, pendingAction?.Id == driver.Name ? Theme.Warn : Theme.SoftText, pendingAction?.Id == driver.Name ? "Enter confirms action, Esc cancels" : "o/x/Delete stop driver  a start driver  c copy name  e export CSV  r refresh");
		}
		else if (drivers.Count == 0)
		{
			canvas.Text(y + 3, x + 2, Theme.Warn, "No drivers match the current filter.");
		}
	}

	private void DrawSecurityPage(Canvas canvas, int x, int y, int w, int h)
	{
		canvas.Box(x, y, w, h, "security / updates / firewall / hardware");
		var security = snapshot.Security;
		var health = ComputeHealthScore();
		var lines = new[]
		{
			$"Health score: {health.Score:0}% ({health.Label})",
			$"Defender real-time: {security.DefenderRealTime}",
			$"Defender antivirus: {security.DefenderAntivirus}",
			$"Signature age: {security.DefenderSignatureAgeDays} days",
			$"Firewall domain/private/public: {security.FirewallDomain} / {security.FirewallPrivate} / {security.FirewallPublic}",
			$"Latest hotfix: {security.LatestHotFix}",
			$"Pending reboot: {security.PendingReboot}",
			$"GPU: {security.GpuSummary}",
			$"Thermal: {security.ThermalSummary}",
			"",
			"c copy summary  e export CSV  r refresh"
		};
		for (var i = 0; i < lines.Length && i < h - 2; i++)
		{
			var color = i switch
			{
				0 => Color.Percent(100 - health.Score),
				1 or 2 or 4 => lines[i].Contains("False", StringComparison.OrdinalIgnoreCase) || lines[i].Contains("Off", StringComparison.OrdinalIgnoreCase) ? Theme.Warn : Theme.Highlight,
				6 => security.PendingReboot ? Theme.Warn : Theme.Ok,
				_ => Theme.Muted
			};
			canvas.Text(y + 1 + i, x + 2, color, Text.Fit(lines[i], Math.Max(5, w - 4)));
		}
	}

	private void DrawTasksPage(Canvas canvas, int x, int y, int w, int h)
	{
		var filterText = string.IsNullOrWhiteSpace(taskFilter) ? "" : $" filter:{taskFilter}";
		canvas.Box(x, y, w, h, $"task scheduler{filterText}");
		var tasks = GetVisibleTasks();
		selectedTask = Math.Clamp(selectedTask, 0, Math.Max(0, tasks.Count - 1));
		var enabled = snapshot.ScheduledTasks.Count(t => t.Enabled);
		var running = snapshot.ScheduledTasks.Count(t => string.Equals(t.State, "Running", StringComparison.OrdinalIgnoreCase));
		var detailRows = h >= 14 ? 4 : 0;
		var visibleRows = Math.Max(0, h - 4 - detailRows);
		KeepSelectionVisible(selectedTask, visibleRows, ref taskScrollOffset);

		canvas.Text(y + 1, x + 2, Theme.Highlight, $"Tasks {snapshot.ScheduledTasks.Count}  Enabled {enabled}  Running {running}");
		canvas.Text(y + 2, x + 2, Theme.Title, "  EN  STATE       LAST RUN           NEXT RUN           PATH / NAME");
		for (var i = 0; i < visibleRows && i + taskScrollOffset < tasks.Count; i++)
		{
			var task = tasks[i + taskScrollOffset];
			var row = y + 3 + i;
			var selected = i + taskScrollOffset == selectedTask;
			var color = selected ? Theme.Selected : task.Enabled ? Theme.Muted : Theme.SoftText;
			var last = task.LastRunTime?.ToString("MM-dd HH:mm") ?? "n/a";
			var next = task.NextRunTime?.ToString("MM-dd HH:mm") ?? "n/a";
			var line = $"{(selected ? ">" : " ")} {(task.Enabled ? "yes" : "no "),-3} {Text.Fit(task.State, 10),-10} {last,-16} {next,-16} {Text.Fit(task.TaskPath + task.TaskName, Math.Max(8, w - 56))}";
			canvas.Text(row, x + 2, color, Text.Fit(line, w - 4));
		}

		if (detailRows > 0 && tasks.Count > 0)
		{
			var task = tasks[selectedTask];
			var top = y + h - detailRows;
			canvas.Text(top, x + 2, Theme.Border, new string('-', Math.Max(1, w - 4)));
			canvas.Text(top + 1, x + 2, Theme.Highlight, Text.Fit($"Selected task: {task.TaskPath}{task.TaskName}", Math.Max(5, w - 4)));
			canvas.Text(top + 2, x + 2, Theme.Muted, Text.Fit($"State {task.State}  Enabled {task.Enabled}  Action: {task.Action}", Math.Max(5, w - 4)));
			canvas.Text(top + 3, x + 2, pendingAction?.Id == task.Id ? Theme.Warn : Theme.SoftText, pendingAction?.Id == task.Id ? "Enter confirms action, Esc cancels" : "x start task  a enable  d/Delete disable  c copy name  e export CSV  r refresh");
		}
		else if (tasks.Count == 0)
		{
			canvas.Text(y + 3, x + 2, Theme.Warn, "No scheduled tasks match the current filter.");
		}
	}

	private void DrawFirewallPage(Canvas canvas, int x, int y, int w, int h)
	{
		var filterText = string.IsNullOrWhiteSpace(firewallFilter) ? "" : $" filter:{firewallFilter}";
		canvas.Box(x, y, w, h, $"firewall rules{filterText}");
		var rules = GetVisibleFirewallRules();
		selectedFirewallRule = Math.Clamp(selectedFirewallRule, 0, Math.Max(0, rules.Count - 1));
		var enabled = snapshot.FirewallRules.Count(r => r.Enabled);
		var detailRows = h >= 14 ? 4 : 0;
		var visibleRows = Math.Max(0, h - 4 - detailRows);
		KeepSelectionVisible(selectedFirewallRule, visibleRows, ref firewallScrollOffset);

		canvas.Text(y + 1, x + 2, Theme.Highlight, $"Rules {snapshot.FirewallRules.Count}  Enabled {enabled}");
		canvas.Text(y + 2, x + 2, Theme.Title, "  EN  DIR       ACTION    PROFILE        DISPLAY NAME");
		for (var i = 0; i < visibleRows && i + firewallScrollOffset < rules.Count; i++)
		{
			var rule = rules[i + firewallScrollOffset];
			var row = y + 3 + i;
			var selected = i + firewallScrollOffset == selectedFirewallRule;
			var color = selected ? Theme.Selected : rule.Enabled ? Theme.Muted : Theme.SoftText;
			var actionColor = string.Equals(rule.Action, "Block", StringComparison.OrdinalIgnoreCase) ? Theme.Warn : color;
			canvas.Text(row, x + 2, color, $"{(selected ? ">" : " ")} {(rule.Enabled ? "yes" : "no "),-3} {Text.Fit(rule.Direction, 9),-9}");
			canvas.Text(row, x + 18, actionColor, $"{Text.Fit(rule.Action, 8),-8}");
			canvas.Text(row, x + 27, color, $"{Text.Fit(rule.Profile, 14),-14} {Text.Fit(rule.DisplayName, Math.Max(8, w - 44))}");
		}

		if (detailRows > 0 && rules.Count > 0)
		{
			var rule = rules[selectedFirewallRule];
			var top = y + h - detailRows;
			canvas.Text(top, x + 2, Theme.Border, new string('-', Math.Max(1, w - 4)));
			canvas.Text(top + 1, x + 2, Theme.Highlight, Text.Fit($"Selected rule: {rule.DisplayName}", Math.Max(5, w - 4)));
			canvas.Text(top + 2, x + 2, Theme.Muted, Text.Fit($"Name {rule.Name}  Enabled {rule.Enabled}  {rule.Direction} {rule.Action} {rule.Profile}  Group {Blank(rule.Group)}", Math.Max(5, w - 4)));
			canvas.Text(top + 3, x + 2, pendingAction?.Id == rule.Name ? Theme.Warn : Theme.SoftText, pendingAction?.Id == rule.Name ? "Enter confirms action, Esc cancels" : "a enable rule  d/x/Delete disable rule  c copy name  e export CSV  r refresh");
		}
		else if (rules.Count == 0)
		{
			canvas.Text(y + 3, x + 2, Theme.Warn, "No firewall rules match the current filter.");
		}
	}

	private void DrawUsersPage(Canvas canvas, int x, int y, int w, int h)
	{
		var filterText = string.IsNullOrWhiteSpace(userFilter) ? "" : $" filter:{userFilter}";
		canvas.Box(x, y, w, h, $"local users / groups{filterText}");
		var users = GetVisibleUsers();
		selectedUser = Math.Clamp(selectedUser, 0, Math.Max(0, users.Count - 1));
		var admins = snapshot.LocalUsers.Count(u => u.IsAdmin);
		var enabled = snapshot.LocalUsers.Count(u => string.Equals(u.Kind, "User", StringComparison.OrdinalIgnoreCase) && u.Enabled);
		var detailRows = h >= 14 ? 4 : 0;
		var visibleRows = Math.Max(0, h - 4 - detailRows);
		KeepSelectionVisible(selectedUser, visibleRows, ref userScrollOffset);

		canvas.Text(y + 1, x + 2, Theme.Highlight, $"Entries {snapshot.LocalUsers.Count}  Enabled users {enabled}  Admin entries {admins}");
		canvas.Text(y + 2, x + 2, Theme.Title, "  KIND    EN  ADM LAST LOGON        NAME / DETAILS");
		for (var i = 0; i < visibleRows && i + userScrollOffset < users.Count; i++)
		{
			var item = users[i + userScrollOffset];
			var row = y + 3 + i;
			var selected = i + userScrollOffset == selectedUser;
			var color = selected ? Theme.Selected : item.IsAdmin ? Theme.Warn : Theme.Muted;
			var last = item.LastLogon?.ToString("MM-dd HH:mm") ?? "n/a";
			var detail = string.Equals(item.Kind, "Group", StringComparison.OrdinalIgnoreCase) ? item.Members : item.Description ?? "";
			var line = $"{(selected ? ">" : " ")} {Text.Fit(item.Kind, 7),-7} {(item.Enabled ? "yes" : "no "),-3} {(item.IsAdmin ? "yes" : "no "),-3} {last,-15} {Text.Fit(item.Name + " :: " + detail, Math.Max(8, w - 42))}";
			canvas.Text(row, x + 2, color, Text.Fit(line, w - 4));
		}

		if (detailRows > 0 && users.Count > 0)
		{
			var item = users[selectedUser];
			var top = y + h - detailRows;
			canvas.Text(top, x + 2, Theme.Border, new string('-', Math.Max(1, w - 4)));
			canvas.Text(top + 1, x + 2, Theme.Highlight, Text.Fit($"Selected {item.Kind}: {item.Name}", Math.Max(5, w - 4)));
			canvas.Text(top + 2, x + 2, Theme.Muted, Text.Fit($"Enabled {item.Enabled}  Admin {item.IsAdmin}  LastLogon {item.LastLogon?.ToString("yyyy-MM-dd HH:mm:ss") ?? "n/a"}  Members: {Blank(item.Members)}", Math.Max(5, w - 4)));
			canvas.Text(top + 3, x + 2, Theme.SoftText, "c copy entry  e export CSV  r refresh");
		}
		else if (users.Count == 0)
		{
			canvas.Text(y + 3, x + 2, Theme.Warn, "No users or groups match the current filter.");
		}
	}

	private static void KeepSelectionVisible(int selected, int visibleRows, ref int scrollOffset)
	{
		if (selected < scrollOffset)
		{
			scrollOffset = selected;
		}
		else if (selected >= scrollOffset + visibleRows)
		{
			scrollOffset = selected - visibleRows + 1;
		}
		scrollOffset = Math.Max(0, scrollOffset);
	}

	private void DrawProcesses(Canvas canvas, int x, int y, int w, int h)
	{
		var filterText = string.IsNullOrWhiteSpace(processFilter) ? "" : $" filter:{processFilter}";
		var treeText = treeMode ? " tree" : "";
		canvas.Box(x, y, w, h, $"processes sort:{sortColumn}{(reverseSort ? " desc" : " asc")}{treeText}{filterText}");
		var sorted = GetVisibleProcesses();

		selectedProcess = Math.Clamp(selectedProcess, 0, Math.Max(0, sorted.Count - 1));
		var detailRows = h >= 20 ? 6 : h >= 18 ? 4 : 0;
		var visibleRows = Math.Max(0, h - 3 - detailRows);
		if (selectedProcess < scrollOffset)
		{
			scrollOffset = selectedProcess;
		}
		else if (selectedProcess >= scrollOffset + visibleRows)
		{
			scrollOffset = selectedProcess - visibleRows + 1;
		}

		canvas.Text(y + 1, x + 2, Theme.Title, " PID     CPU%     MEM       THR   NAME");
		for (var i = 0; i < visibleRows && i + scrollOffset < sorted.Count; i++)
		{
			var process = sorted[i + scrollOffset];
			var row = y + 2 + i;
			var selected = i + scrollOffset == selectedProcess;
			var color = selected ? Theme.Selected : (process.CpuPercent > 15 ? Theme.Cpu : Theme.Muted);
			var prefix = selected ? ">" : " ";
			var nameWidth = Math.Max(8, w - 39);
			var depth = GetTreeDepth(process);
			var treePrefix = treeMode ? new string(' ', Math.Min(depth * 2, Math.Max(0, nameWidth - 3))) + (depth > 0 ? "└ " : "") : "";
			var line = $"{prefix}{process.Pid,6} {process.CpuPercent,7:0.0} {Units.Size(process.MemoryBytes),10} {process.ThreadCount,5} {Text.Fit(treePrefix + process.Name, nameWidth)}";
			canvas.Text(row, x + 2, color, Text.Fit(line, w - 4));
		}

		if (detailRows > 0 && sorted.Count > 0)
		{
			var process = sorted[selectedProcess];
			var top = y + h - detailRows;
			canvas.Text(top, x + 2, Theme.Border, new string('─', Math.Max(1, w - 4)));
			canvas.Text(top + 1, x + 2, Theme.Highlight, $"Selected PID {process.Pid}  PPID {process.ParentPid}  {process.Name}");
			canvas.Text(top + 1, x + Math.Min(w - 38, 42), process.Responding ? Theme.Muted : Theme.Warn, $"CPU {process.CpuPercent:0.0}%  MEM {Units.Size(process.MemoryBytes)}  THR {process.ThreadCount}  HND {process.HandleCount}");
			var started = process.StartTime is null ? "n/a" : process.StartTime.Value.ToString("yyyy-MM-dd HH:mm:ss");
			canvas.Text(top + 2, x + 2, Theme.Muted, Text.Fit($"Priority {process.Priority}  Started {started}  Path: {process.Path ?? "n/a"}", Math.Max(5, w - 4)));
			var processPending = pendingAction?.Kind == PendingActionKind.KillProcess && pendingAction?.Id == process.Pid.ToString(CultureInfo.InvariantCulture);
			if (detailRows >= 6)
			{
				var graphWidth = Math.Max(10, (w - 24) / 2);
				var memMax = Math.Max(0.1, process.MemoryHistory.Count == 0 ? 0.1 : process.MemoryHistory.Max());
				canvas.Text(top + 3, x + 2, Theme.Cpu, Text.Fit($"CPU {Graph.Line(process.CpuHistory, graphWidth, 100)}", Math.Max(5, graphWidth + 5)));
				canvas.Text(top + 3, x + graphWidth + 10, Theme.Memory, Text.Fit($"RAM {Graph.Line(process.MemoryHistory, graphWidth, memMax)}", Math.Max(5, graphWidth + 5)));
				canvas.Text(top + 4, x + 2, Theme.Muted, $"Network conns {process.NetworkConnectionCount}  established {process.EstablishedConnectionCount}");
				canvas.Text(top + 5, x + 2, processPending ? Theme.Warn : Theme.SoftText, processPending ? "Enter confirms action, Esc cancels" : "x kill  b kill tree  z suspend  u resume  p AboveNormal  n Normal  c copy  e export");
			}
			else
			{
				canvas.Text(top + 3, x + 2, processPending ? Theme.Warn : Theme.SoftText, processPending ? "Enter confirms kill, Esc cancels" : "x kill  b kill tree  z suspend  u resume  p/n priority  c copy  e export");
			}
		}
		else if (sorted.Count == 0)
		{
			canvas.Text(y + 2, x + 2, Theme.Warn, "No processes match the current filter.");
		}
	}

	private static void DrawHelp(Canvas canvas, int width, int height)
	{
		var lines = new[]
		{
			"q / Esc     quit",
			"h / F1      toggle this help",
			"Space       pause updates",
			"1..9        monitor, USB, services, network, startup, events, drivers, security, tasks",
			"F9/F10      firewall rules, local users/groups",
			"/ or f      edit filter for the current page",
			"0           clear current page filter",
			"s           save current snapshot as JSON",
			"+ / -       change update interval",
			"Up/Down     move selection, PageUp/PageDown faster, Home/End first/last",
			"",
			"Monitor page:",
			"Tab         cycle process sort column",
			"r           reverse process sort",
			"t           toggle process tree",
			"p / n       set selected process priority AboveNormal / Normal",
			"b / z / u   kill process tree, suspend process, resume process",
			"c           copy selected executable path",
			"e           export visible processes as CSV",
			"x / Delete  arm kill for selected process",
			"",
			"USB page:",
			"d/x/Delete  disable selected USB device",
			"a           enable selected USB device",
			"c           copy selected USB InstanceId",
			"e           export USB devices and drivers as CSV",
			"r           refresh USB inventory",
			"",
			"Services page:",
			"o/x/Delete  stop selected service",
			"r           restart selected service",
			"a           start selected service",
			"c           copy selected service name",
			"e           export visible services as CSV",
			"",
			"Network: x kill owner, c copy endpoint, e export, r refresh",
			"Startup: c copy command, e export, r refresh",
			"Events:  c copy event, e export, r refresh",
			"Drivers: o/x stop, a start, c copy name, e export, r refresh",
			"Security: c copy summary, e export, r refresh",
			"Tasks:    x start, a enable, d disable, c copy, e export, r refresh",
			"Firewall: a enable, d/x disable, c copy, e export, r refresh",
			"Users:    c copy, e export, r refresh",
			"Enter       confirm pending action, Esc cancels"
		};
		var w = Math.Min(82, width - 8);
		var h = Math.Min(lines.Length + 2, height - 4);
		var x = (width - w) / 2 + 1;
		var y = (height - h) / 2 + 1;
		canvas.Box(x, y, w, h, "help");
		var visibleLines = Math.Max(0, h - 2);
		for (var i = 0; i < visibleLines && i < lines.Length; i++)
		{
			canvas.Text(y + 1 + i, x + 2, Theme.Highlight, lines[i]);
		}
	}

	private static string Meter(double percent, int width)
	{
		percent = Math.Clamp(percent, 0, 100);
		var filled = (int)Math.Round(width * percent / 100);
		return new string('█', filled) + new string('░', Math.Max(0, width - filled));
	}
}

internal sealed class Sampler : IDisposable
{
	private readonly bool synchronousInventory;
	private readonly CpuReader cpuReader = new();
	private readonly object inventoryLock = new();
	private readonly Dictionary<string, NetCounter> netCounters = new();
	private readonly Dictionary<int, TimeSpan> processCpu = new();
	private readonly Dictionary<int, List<double>> processCpuHistory = new();
	private readonly Dictionary<int, List<double>> processMemHistory = new();
	private readonly List<double> cpuHistory = new();
	private readonly List<double> memHistory = new();
	private readonly List<double> netHistory = new();
	private IReadOnlyList<UsbDeviceSnapshot> usbDevices = [];
	private IReadOnlyList<UsbDriverSnapshot> usbDrivers = [];
	private IReadOnlyList<ServiceSnapshot> services = [];
	private IReadOnlyList<NetworkConnectionSnapshot> networkConnections = [];
	private IReadOnlyList<StartupItemSnapshot> startupItems = [];
	private IReadOnlyList<EventSnapshot> events = [];
	private IReadOnlyList<DriverSnapshot> drivers = [];
	private IReadOnlyList<ScheduledTaskSnapshot> tasks = [];
	private IReadOnlyList<FirewallRuleSnapshot> firewallRules = [];
	private IReadOnlyList<LocalUserSnapshot> users = [];
	private SecuritySnapshot security = SecuritySnapshot.Empty;
	private long totalDownloadedBytes;
	private long totalUploadedBytes;
	private DateTime lastNetTime = DateTime.UtcNow;
	private DateTime lastProcessTime = DateTime.UtcNow;
	private DateTime nextInventoryRead = DateTime.MinValue;
	private ViewMode activeView = ViewMode.Monitor;
	private ViewMode lastInventoryView = ViewMode.Monitor;
	private Task? inventoryTask;
	private bool disposed;

	public Sampler(bool synchronousInventory = false)
	{
		this.synchronousInventory = synchronousInventory;
	}

	public Snapshot Read()
	{
		var cpu = cpuReader.Read();
		AddHistory(cpuHistory, cpu.TotalPercent, 240);

		var memory = MemoryReader.Read();
		AddHistory(memHistory, memory.UsedPercent, 240);
		memory = memory with { History = cpuHistory.Count == 0 ? [] : [.. memHistory] };

		var network = ReadNetwork();
		AddHistory(netHistory, network.DownloadBytesPerSec + network.UploadBytesPerSec, 240);
		network = network with
		{
			History = [.. netHistory],
			HistoryMax = Math.Max(1, netHistory.Count == 0 ? 1 : netHistory.Max())
		};

		var system = SystemReader.Read();
		var disks = ReadDisks();
		var processes = ReadProcesses();
		ReadInventoryIfNeeded();
		IReadOnlyList<UsbDeviceSnapshot> usbDevicesSnapshot;
		IReadOnlyList<UsbDriverSnapshot> usbDriversSnapshot;
		IReadOnlyList<ServiceSnapshot> servicesSnapshot;
		IReadOnlyList<NetworkConnectionSnapshot> networkConnectionsSnapshot;
		IReadOnlyList<StartupItemSnapshot> startupItemsSnapshot;
		IReadOnlyList<EventSnapshot> eventsSnapshot;
		IReadOnlyList<DriverSnapshot> driversSnapshot;
		IReadOnlyList<ScheduledTaskSnapshot> tasksSnapshot;
		IReadOnlyList<FirewallRuleSnapshot> firewallRulesSnapshot;
		IReadOnlyList<LocalUserSnapshot> usersSnapshot;
		SecuritySnapshot securitySnapshot;
		lock (inventoryLock)
		{
			usbDevicesSnapshot = usbDevices;
			usbDriversSnapshot = usbDrivers;
			servicesSnapshot = services;
			networkConnectionsSnapshot = networkConnections;
			startupItemsSnapshot = startupItems;
			eventsSnapshot = events;
			driversSnapshot = drivers;
			tasksSnapshot = tasks;
			firewallRulesSnapshot = firewallRules;
			usersSnapshot = users;
			securitySnapshot = security;
		}

		return new Snapshot(
			DateTime.Now,
			system,
			cpu with { History = [.. cpuHistory] },
			memory,
			network,
			disks,
			processes,
			usbDevicesSnapshot,
			usbDriversSnapshot,
			servicesSnapshot,
			networkConnectionsSnapshot,
			startupItemsSnapshot,
			eventsSnapshot,
			driversSnapshot,
			tasksSnapshot,
			firewallRulesSnapshot,
			usersSnapshot,
			securitySnapshot);
	}

	public void Dispose()
	{
		disposed = true;
	}

	public void RefreshInventory()
	{
		nextInventoryRead = DateTime.MinValue;
	}

	public void SetActiveView(ViewMode mode)
	{
		if (activeView == mode)
		{
			return;
		}

		activeView = mode;
		nextInventoryRead = DateTime.MinValue;
	}

	private void ReadInventoryIfNeeded()
	{
		var now = DateTime.UtcNow;
		if (activeView != lastInventoryView)
		{
			nextInventoryRead = DateTime.MinValue;
		}
		if (now < nextInventoryRead)
		{
			return;
		}

		lastInventoryView = activeView;
		nextInventoryRead = now.AddSeconds(synchronousInventory ? 10 : 90);
		if (synchronousInventory)
		{
			ApplyInventory(ReadInventorySnapshot());
			return;
		}

		if (inventoryTask is { IsCompleted: false })
		{
			return;
		}

		var viewToRead = activeView;
		inventoryTask = Task.Run(() =>
		{
			if (!disposed)
			{
				ReadInventoryForView(viewToRead);
			}
		});
	}

	private void ReadInventoryForView(ViewMode mode)
	{
		switch (mode)
		{
			case ViewMode.Usb:
			{
				var devices = SafeRead(WindowsInventory.ReadUsbDevices, Array.Empty<UsbDeviceSnapshot>());
				var usbDriverList = SafeRead(WindowsInventory.ReadUsbDrivers, Array.Empty<UsbDriverSnapshot>());
				lock (inventoryLock)
				{
					usbDevices = devices;
					usbDrivers = usbDriverList;
				}
				break;
			}
			case ViewMode.Services:
			{
				var serviceList = SafeRead(WindowsInventory.ReadServices, Array.Empty<ServiceSnapshot>());
				lock (inventoryLock)
				{
					services = serviceList;
				}
				break;
			}
			case ViewMode.Network:
			{
				var connectionList = SafeRead(WindowsInventory.ReadNetworkConnections, Array.Empty<NetworkConnectionSnapshot>());
				lock (inventoryLock)
				{
					networkConnections = connectionList;
				}
				break;
			}
			case ViewMode.Startup:
			{
				var itemList = SafeRead(WindowsInventory.ReadStartupItems, Array.Empty<StartupItemSnapshot>());
				lock (inventoryLock)
				{
					startupItems = itemList;
				}
				break;
			}
			case ViewMode.Events:
			{
				var eventList = SafeRead(WindowsInventory.ReadEvents, Array.Empty<EventSnapshot>());
				lock (inventoryLock)
				{
					events = eventList;
				}
				break;
			}
			case ViewMode.Drivers:
			{
				var driverList = SafeRead(WindowsInventory.ReadDrivers, Array.Empty<DriverSnapshot>());
				lock (inventoryLock)
				{
					drivers = driverList;
				}
				break;
			}
			case ViewMode.Security:
			{
				var securitySnapshot = SafeRead(WindowsInventory.ReadSecurity, SecuritySnapshot.Empty);
				var eventList = SafeRead(WindowsInventory.ReadEvents, Array.Empty<EventSnapshot>());
				lock (inventoryLock)
				{
					security = securitySnapshot;
					events = eventList;
				}
				break;
			}
			case ViewMode.Tasks:
			{
				var taskList = SafeRead(WindowsInventory.ReadScheduledTasks, Array.Empty<ScheduledTaskSnapshot>());
				lock (inventoryLock)
				{
					tasks = taskList;
				}
				break;
			}
			case ViewMode.Firewall:
			{
				var ruleList = SafeRead(WindowsInventory.ReadFirewallRules, Array.Empty<FirewallRuleSnapshot>());
				lock (inventoryLock)
				{
					firewallRules = ruleList;
				}
				break;
			}
			case ViewMode.Users:
			{
				var userList = SafeRead(WindowsInventory.ReadLocalUsers, Array.Empty<LocalUserSnapshot>());
				lock (inventoryLock)
				{
					users = userList;
				}
				break;
			}
		}
	}

	private void ApplyInventory(InventorySnapshot inventory)
	{
		lock (inventoryLock)
		{
			usbDevices = inventory.UsbDevices;
			usbDrivers = inventory.UsbDrivers;
			services = inventory.Services;
			networkConnections = inventory.NetworkConnections;
			startupItems = inventory.StartupItems;
			events = inventory.Events;
			drivers = inventory.Drivers;
			tasks = inventory.Tasks;
			firewallRules = inventory.FirewallRules;
			users = inventory.Users;
			security = inventory.Security;
		}
	}

	private static InventorySnapshot ReadInventorySnapshot()
	{
		return new InventorySnapshot(
			SafeRead(WindowsInventory.ReadUsbDevices, Array.Empty<UsbDeviceSnapshot>()),
			SafeRead(WindowsInventory.ReadUsbDrivers, Array.Empty<UsbDriverSnapshot>()),
			SafeRead(WindowsInventory.ReadServices, Array.Empty<ServiceSnapshot>()),
			SafeRead(WindowsInventory.ReadNetworkConnections, Array.Empty<NetworkConnectionSnapshot>()),
			SafeRead(WindowsInventory.ReadStartupItems, Array.Empty<StartupItemSnapshot>()),
			SafeRead(WindowsInventory.ReadEvents, Array.Empty<EventSnapshot>()),
			SafeRead(WindowsInventory.ReadDrivers, Array.Empty<DriverSnapshot>()),
			SafeRead(WindowsInventory.ReadScheduledTasks, Array.Empty<ScheduledTaskSnapshot>()),
			SafeRead(WindowsInventory.ReadFirewallRules, Array.Empty<FirewallRuleSnapshot>()),
			SafeRead(WindowsInventory.ReadLocalUsers, Array.Empty<LocalUserSnapshot>()),
			SafeRead(WindowsInventory.ReadSecurity, SecuritySnapshot.Empty));
	}

	private static T SafeRead<T>(Func<T> read, T fallback)
	{
		try
		{
			return read();
		}
		catch
		{
			return fallback;
		}
	}

	private NetworkSnapshot ReadNetwork()
	{
		var now = DateTime.UtcNow;
		var seconds = Math.Max(0.1, (now - lastNetTime).TotalSeconds);
		lastNetTime = now;

		var adapters = new List<AdapterSnapshot>();
		double totalDown = 0;
		double totalUp = 0;
		var liveIds = new HashSet<string>();

		foreach (var nic in NetworkInterface.GetAllNetworkInterfaces())
		{
			if (nic.OperationalStatus != OperationalStatus.Up ||
			    nic.NetworkInterfaceType == NetworkInterfaceType.Loopback ||
			    nic.NetworkInterfaceType == NetworkInterfaceType.Tunnel)
			{
				continue;
			}

			IPv4InterfaceStatistics stats;
			try
			{
				stats = nic.GetIPv4Statistics();
			}
			catch
			{
				continue;
			}

			liveIds.Add(nic.Id);
			var current = new NetCounter(stats.BytesReceived, stats.BytesSent);
			if (!netCounters.TryGetValue(nic.Id, out var previous))
			{
				netCounters[nic.Id] = current;
				continue;
			}

			var receivedDelta = Math.Max(0, current.Received - previous.Received);
			var sentDelta = Math.Max(0, current.Sent - previous.Sent);
			var down = receivedDelta / seconds;
			var up = sentDelta / seconds;
			netCounters[nic.Id] = current;

			totalDown += down;
			totalUp += up;
			totalDownloadedBytes += receivedDelta;
			totalUploadedBytes += sentDelta;
			adapters.Add(new AdapterSnapshot(nic.Name, down, up));
		}

		foreach (var id in netCounters.Keys.Where(id => !liveIds.Contains(id)).ToList())
		{
			netCounters.Remove(id);
		}

		return new NetworkSnapshot(totalDown, totalUp, totalDownloadedBytes, totalUploadedBytes, adapters.OrderByDescending(a => a.DownloadBytesPerSec + a.UploadBytesPerSec).ToList(), [], 1);
	}

	private static List<DiskSnapshot> ReadDisks()
	{
		var disks = new List<DiskSnapshot>();
		foreach (var drive in DriveInfo.GetDrives())
		{
			if (!drive.IsReady)
			{
				continue;
			}

			try
			{
				var used = drive.TotalSize - drive.AvailableFreeSpace;
				var usedPercent = drive.TotalSize <= 0 ? 0 : used * 100.0 / drive.TotalSize;
				disks.Add(new DiskSnapshot(drive.Name.TrimEnd('\\'), drive.DriveFormat, drive.TotalSize, used, drive.AvailableFreeSpace, usedPercent));
			}
			catch
			{
				// Some virtual drives can disappear while being queried.
			}
		}

		return disks.OrderByDescending(d => d.UsedPercent).ToList();
	}

	private List<ProcessSnapshot> ReadProcesses()
	{
		var now = DateTime.UtcNow;
		var seconds = Math.Max(0.1, (now - lastProcessTime).TotalSeconds);
		lastProcessTime = now;
		var processorCount = Math.Max(1, Environment.ProcessorCount);
		var result = new List<ProcessSnapshot>();
		var livePids = new HashSet<int>();
		var parentPids = NativeMethods.ReadParentProcessIds();
		var connectionCounts = networkConnections
			.GroupBy(c => c.ProcessId)
			.ToDictionary(group => group.Key, group => group.Count());
		var establishedCounts = networkConnections
			.Where(c => string.Equals(c.State, "Established", StringComparison.OrdinalIgnoreCase))
			.GroupBy(c => c.ProcessId)
			.ToDictionary(group => group.Key, group => group.Count());

		foreach (var process in Process.GetProcesses())
		{
			using (process)
			{
				try
				{
					var pid = process.Id;
					var total = process.TotalProcessorTime;
					var memory = process.WorkingSet64;
					var name = process.ProcessName;
					var threadCount = 0;
					var handleCount = 0;
					var responding = true;
					var priority = "n/a";
					DateTime? startTime = null;
					string? path = null;
					try
					{
						threadCount = process.Threads.Count;
					}
					catch
					{
						// Access can be denied for protected processes.
					}
					try
					{
						handleCount = process.HandleCount;
						responding = process.Responding;
						priority = process.PriorityClass.ToString();
						startTime = process.StartTime;
					}
					catch
					{
						// Process details are best effort.
					}
					try
					{
						path = process.MainModule?.FileName;
					}
					catch
					{
						// Access can be denied for protected processes.
					}
					livePids.Add(pid);

					var cpu = 0.0;
					if (processCpu.TryGetValue(pid, out var previous))
					{
						cpu = Math.Max(0, (total - previous).TotalSeconds / seconds * 100.0 / processorCount);
					}
					processCpu[pid] = total;
					if (!processCpuHistory.TryGetValue(pid, out var cpuHistoryForProcess))
					{
						cpuHistoryForProcess = [];
						processCpuHistory[pid] = cpuHistoryForProcess;
					}
					if (!processMemHistory.TryGetValue(pid, out var memHistoryForProcess))
					{
						memHistoryForProcess = [];
						processMemHistory[pid] = memHistoryForProcess;
					}
					AddHistory(cpuHistoryForProcess, cpu, 90);
					AddHistory(memHistoryForProcess, memory / Units.Gib, 90);

					result.Add(new ProcessSnapshot(pid, parentPids.GetValueOrDefault(pid), name, cpu, memory, threadCount, handleCount, priority, responding, startTime, path, connectionCounts.GetValueOrDefault(pid), establishedCounts.GetValueOrDefault(pid), [.. cpuHistoryForProcess], [.. memHistoryForProcess]));
				}
				catch
				{
					// Protected or short-lived processes can fail between enumeration and read.
				}
			}
		}

		foreach (var pid in processCpu.Keys.Where(pid => !livePids.Contains(pid)).ToList())
		{
			processCpu.Remove(pid);
			processCpuHistory.Remove(pid);
			processMemHistory.Remove(pid);
		}

		return result;
	}

	private static void AddHistory(List<double> values, double value, int max)
	{
		values.Add(value);
		if (values.Count > max)
		{
			values.RemoveRange(0, values.Count - max);
		}
	}

	private readonly record struct NetCounter(long Received, long Sent);
	private readonly record struct InventorySnapshot(
		IReadOnlyList<UsbDeviceSnapshot> UsbDevices,
		IReadOnlyList<UsbDriverSnapshot> UsbDrivers,
		IReadOnlyList<ServiceSnapshot> Services,
		IReadOnlyList<NetworkConnectionSnapshot> NetworkConnections,
		IReadOnlyList<StartupItemSnapshot> StartupItems,
		IReadOnlyList<EventSnapshot> Events,
		IReadOnlyList<DriverSnapshot> Drivers,
		IReadOnlyList<ScheduledTaskSnapshot> Tasks,
		IReadOnlyList<FirewallRuleSnapshot> FirewallRules,
		IReadOnlyList<LocalUserSnapshot> Users,
		SecuritySnapshot Security);
}

internal sealed class CpuReader
{
	private ProcessorTimes[]? previous;

	public CpuSnapshot Read()
	{
		var current = NativeMethods.ReadProcessorTimes();
		if (previous is null || previous.Length != current.Length)
		{
			previous = current;
			return new CpuSnapshot(0, new double[current.Length], []);
		}

		var percents = new double[current.Length];
		long totalDelta = 0;
		long busyDelta = 0;

		for (var i = 0; i < current.Length; i++)
		{
			var total = current[i].Total - previous[i].Total;
			var idle = current[i].Idle - previous[i].Idle;
			var busy = Math.Max(0, total - idle);
			percents[i] = total <= 0 ? 0 : Math.Clamp(busy * 100.0 / total, 0, 100);
			totalDelta += total;
			busyDelta += busy;
		}

		previous = current;
		var totalPercent = totalDelta <= 0 ? 0 : Math.Clamp(busyDelta * 100.0 / totalDelta, 0, 100);
		return new CpuSnapshot(totalPercent, percents, []);
	}
}

internal static class SystemReader
{
	public static SystemSnapshot Read()
	{
		int? batteryPercent = null;
		bool? onAcPower = null;
		var status = new NativeMethods.SystemPowerStatus();
		if (NativeMethods.GetSystemPowerStatus(out status))
		{
			if (status.BatteryLifePercent <= 100)
			{
				batteryPercent = status.BatteryLifePercent;
			}
			if (status.AcLineStatus <= 1)
			{
				onAcPower = status.AcLineStatus == 1;
			}
		}

		return new SystemSnapshot(
			Environment.MachineName,
			RuntimeInformation.OSDescription.Trim(),
			TimeSpan.FromMilliseconds(Environment.TickCount64),
			batteryPercent,
			onAcPower);
	}
}

internal static class MemoryReader
{
	public static MemorySnapshot Read()
	{
		var status = new NativeMethods.MemoryStatusEx();
		status.Length = (uint)Marshal.SizeOf<NativeMethods.MemoryStatusEx>();
		if (!NativeMethods.GlobalMemoryStatusEx(ref status))
		{
			return new MemorySnapshot(0, 0, 0, 0, 0, 0, []);
		}

		var used = status.TotalPhys - status.AvailPhys;
		var pageFileUsed = status.TotalPageFile - status.AvailPageFile;
		var percent = status.TotalPhys == 0 ? 0 : used * 100.0 / status.TotalPhys;
		return new MemorySnapshot(status.TotalPhys, used, status.AvailPhys, status.TotalPageFile, pageFileUsed, percent, []);
	}
}

internal static class WindowsInventory
{
	private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true };

	public static IReadOnlyList<UsbDeviceSnapshot> ReadUsbDevices()
	{
		var script = """
			$ErrorActionPreference = 'Stop'
			$all = @(Get-PnpDevice | Where-Object { $_.InstanceId -like 'USB*' -or $_.Class -eq 'USB' -or $_.Class -eq 'USBDevice' })
			$presentIds = @(Get-PnpDevice -PresentOnly | Where-Object { $_.InstanceId -like 'USB*' -or $_.Class -eq 'USB' -or $_.Class -eq 'USBDevice' } | ForEach-Object { $_.InstanceId })
			$present = @{}
			foreach ($id in $presentIds) { $present[$id] = $true }
			@($all | Sort-Object Class,FriendlyName | Select-Object `
				@{Name='Name';Expression={ if ($_.FriendlyName) { $_.FriendlyName } else { $_.InstanceId } }}, `
				@{Name='Status';Expression={ [string]$_.Status }}, `
				@{Name='Class';Expression={ [string]$_.Class }}, `
				@{Name='InstanceId';Expression={ [string]$_.InstanceId }}, `
				@{Name='Present';Expression={ $present.ContainsKey($_.InstanceId) }}) | ConvertTo-Json -Compress -Depth 3
			""";
		return DeserializeList<UsbDeviceDto>(RunPowerShell(script))
			.Select(d => new UsbDeviceSnapshot(d.Name ?? "n/a", d.Status ?? "n/a", d.Class ?? "n/a", d.InstanceId ?? "", d.Present))
			.Where(d => !string.IsNullOrWhiteSpace(d.InstanceId))
			.ToList();
	}

	public static IReadOnlyList<UsbDriverSnapshot> ReadUsbDrivers()
	{
		var script = """
			$ErrorActionPreference = 'Stop'
			@(Get-CimInstance Win32_PnPSignedDriver |
				Where-Object { $_.DeviceID -like 'USB*' -or $_.DeviceClass -eq 'USB' -or $_.DeviceClass -eq 'USBDevice' } |
				Sort-Object DeviceName |
				Select-Object `
					@{Name='DeviceName';Expression={ if ($_.DeviceName) { $_.DeviceName } else { $_.DeviceID } }}, `
					@{Name='Provider';Expression={ [string]$_.DriverProviderName }}, `
					@{Name='Version';Expression={ [string]$_.DriverVersion }}, `
					@{Name='InfName';Expression={ [string]$_.InfName }}, `
					@{Name='DeviceClass';Expression={ [string]$_.DeviceClass }}, `
					@{Name='DeviceId';Expression={ [string]$_.DeviceID }}) | ConvertTo-Json -Compress -Depth 3
			""";
		return DeserializeList<UsbDriverDto>(RunPowerShell(script))
			.Select(d => new UsbDriverSnapshot(d.DeviceName ?? "n/a", d.Provider ?? "n/a", d.Version ?? "n/a", d.InfName ?? "n/a", d.DeviceClass ?? "n/a", d.DeviceId ?? ""))
			.ToList();
	}

	public static IReadOnlyList<ServiceSnapshot> ReadServices()
	{
		var script = """
			$ErrorActionPreference = 'Stop'
			$cim = @{}
			foreach ($svc in Get-CimInstance Win32_Service) { $cim[$svc.Name] = $svc }
			@(Get-Service |
				Sort-Object Name |
				Select-Object `
					@{Name='Name';Expression={ [string]$_.Name }},
					@{Name='DisplayName';Expression={ [string]$_.DisplayName }},
					@{Name='State';Expression={ [string]$_.Status }},
					@{Name='StartMode';Expression={ if ($cim.ContainsKey($_.Name)) { [string]$cim[$_.Name].StartMode } else { '' } }},
					@{Name='AcceptStop';Expression={ [bool]$_.CanStop }},
					@{Name='ProcessId';Expression={ if ($cim.ContainsKey($_.Name)) { [int]$cim[$_.Name].ProcessId } else { 0 } }},
					@{Name='DependsOn';Expression={ [string](($_.ServicesDependedOn | ForEach-Object Name) -join ';') }},
					@{Name='DependedOnBy';Expression={ [string](($_.DependentServices | ForEach-Object Name) -join ';') }},
					@{Name='Path';Expression={ if ($cim.ContainsKey($_.Name)) { [string]$cim[$_.Name].PathName } else { '' } }}) | ConvertTo-Json -Compress -Depth 3
			""";
		return DeserializeList<ServiceDto>(RunPowerShell(script))
			.Select(s => new ServiceSnapshot(s.Name ?? "", s.DisplayName ?? s.Name ?? "n/a", s.State ?? "n/a", s.StartMode ?? "n/a", s.AcceptStop, s.ProcessId, s.DependsOn ?? "", s.DependedOnBy ?? "", s.Path))
			.Where(s => !string.IsNullOrWhiteSpace(s.Name))
			.ToList();
	}

	public static IReadOnlyList<NetworkConnectionSnapshot> ReadNetworkConnections()
	{
		var script = """
			$ErrorActionPreference = 'SilentlyContinue'
			$rows = @()
			foreach ($c in @(Get-NetTCPConnection)) {
				$p = Get-Process -Id $c.OwningProcess -ErrorAction SilentlyContinue
				$rows += [pscustomobject]@{
					Protocol='TCP'
					State=[string]$c.State
					LocalEndpoint=('{0}:{1}' -f $c.LocalAddress,$c.LocalPort)
					RemoteEndpoint=('{0}:{1}' -f $c.RemoteAddress,$c.RemotePort)
					ProcessId=[int]$c.OwningProcess
					ProcessName= if ($p) { [string]$p.ProcessName } else { '' }
				}
			}
			foreach ($u in @(Get-NetUDPEndpoint)) {
				$p = Get-Process -Id $u.OwningProcess -ErrorAction SilentlyContinue
				$rows += [pscustomobject]@{
					Protocol='UDP'
					State='Listen'
					LocalEndpoint=('{0}:{1}' -f $u.LocalAddress,$u.LocalPort)
					RemoteEndpoint='*:*'
					ProcessId=[int]$u.OwningProcess
					ProcessName= if ($p) { [string]$p.ProcessName } else { '' }
				}
			}
			@($rows | Sort-Object Protocol,State,LocalEndpoint) | ConvertTo-Json -Compress -Depth 3
			""";
		return DeserializeList<NetworkConnectionDto>(RunPowerShell(script, 20_000))
			.Select(c => new NetworkConnectionSnapshot(c.Protocol ?? "", c.State ?? "", c.LocalEndpoint ?? "", c.RemoteEndpoint ?? "", c.ProcessId, string.IsNullOrWhiteSpace(c.ProcessName) ? "n/a" : c.ProcessName))
			.ToList();
	}

	public static IReadOnlyList<StartupItemSnapshot> ReadStartupItems()
	{
		var script = """
			$ErrorActionPreference = 'SilentlyContinue'
			$rows = @()
			foreach ($item in @(Get-CimInstance Win32_StartupCommand)) {
				$rows += [pscustomobject]@{ Name=[string]$item.Name; Source='StartupCommand'; Location=[string]$item.Location; Command=[string]$item.Command }
			}
			foreach ($task in @(Get-ScheduledTask | Where-Object { $_.Triggers -and ($_.Triggers | Where-Object { $_.CimClass.CimClassName -match 'Logon|Boot|Startup' }) })) {
				$rows += [pscustomobject]@{ Name=[string]$task.TaskName; Source='ScheduledTask'; Location=[string]$task.TaskPath; Command=[string](($task.Actions | ForEach-Object { ($_.Execute + ' ' + $_.Arguments).Trim() }) -join '; ') }
			}
			@($rows | Sort-Object Source,Name) | ConvertTo-Json -Compress -Depth 3
			""";
		return DeserializeList<StartupItemDto>(RunPowerShell(script, 20_000))
			.Select(i => new StartupItemSnapshot(i.Name ?? "n/a", i.Source ?? "n/a", i.Location ?? "", i.Command ?? ""))
			.ToList();
	}

	public static IReadOnlyList<EventSnapshot> ReadEvents()
	{
		var script = """
			$ErrorActionPreference = 'SilentlyContinue'
			$start = (Get-Date).AddHours(-24)
			@(Get-WinEvent -FilterHashtable @{LogName=@('System','Application'); Level=@(2,3); StartTime=$start} -MaxEvents 80 |
				Select-Object `
					@{Name='TimeCreated';Expression={ if ($_.TimeCreated) { $_.TimeCreated.ToString('o') } else { $null } }},
					@{Name='LogName';Expression={[string]$_.LogName}},
					@{Name='Level';Expression={[string]$_.LevelDisplayName}},
					@{Name='Provider';Expression={[string]$_.ProviderName}},
					@{Name='Id';Expression={[int]$_.Id}},
					@{Name='Message';Expression={([string]$_.Message).Replace("`r",' ').Replace("`n",' ')}}) | ConvertTo-Json -Compress -Depth 3
			""";
		return DeserializeList<EventDto>(RunPowerShell(script, 20_000))
			.Select(e => new EventSnapshot(e.TimeCreated, e.LogName ?? "", e.Level ?? "", e.Provider ?? "", e.Id, e.Message ?? ""))
			.ToList();
	}

	public static IReadOnlyList<DriverSnapshot> ReadDrivers()
	{
		var script = """
			$ErrorActionPreference = 'Stop'
			@(Get-CimInstance Win32_SystemDriver |
				Sort-Object Name |
				Select-Object `
					@{Name='Name';Expression={[string]$_.Name}},
					@{Name='DisplayName';Expression={ if ($_.DisplayName) { [string]$_.DisplayName } else { [string]$_.Name } }},
					@{Name='State';Expression={[string]$_.State}},
					@{Name='StartMode';Expression={[string]$_.StartMode}},
					@{Name='Path';Expression={[string]$_.PathName}}) | ConvertTo-Json -Compress -Depth 3
			""";
		return DeserializeList<DriverDto>(RunPowerShell(script, 20_000))
			.Select(d => new DriverSnapshot(d.Name ?? "", d.DisplayName ?? d.Name ?? "n/a", d.State ?? "n/a", d.StartMode ?? "n/a", d.Path))
			.Where(d => !string.IsNullOrWhiteSpace(d.Name))
			.ToList();
	}

	public static SecuritySnapshot ReadSecurity()
	{
		var script = """
			$ErrorActionPreference = 'SilentlyContinue'
			$mp = Get-MpComputerStatus
			$fw = @{}
			foreach ($profile in @(Get-NetFirewallProfile)) { $fw[$profile.Name] = [string]$profile.Enabled }
			$hotfix = Get-HotFix | Sort-Object InstalledOn -Descending | Select-Object -First 1
			$pending = Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending'
			$gpus = @(Get-CimInstance Win32_VideoController | ForEach-Object { '{0} ({1})' -f $_.Name,$_.DriverVersion })
			$thermal = @(Get-CimInstance -Namespace root/wmi -ClassName MSAcpi_ThermalZoneTemperature | ForEach-Object { '{0:0.0} C' -f (($_.CurrentTemperature / 10) - 273.15) })
			[pscustomobject]@{
				DefenderRealTime= if ($mp) { [string]$mp.RealTimeProtectionEnabled } else { 'n/a' }
				DefenderAntivirus= if ($mp) { [string]$mp.AntivirusEnabled } else { 'n/a' }
				DefenderSignatureAgeDays= if ($mp) { [int]$mp.AntispywareSignatureAge } else { -1 }
				FirewallDomain= if ($fw.ContainsKey('Domain')) { $fw['Domain'] } else { 'n/a' }
				FirewallPrivate= if ($fw.ContainsKey('Private')) { $fw['Private'] } else { 'n/a' }
				FirewallPublic= if ($fw.ContainsKey('Public')) { $fw['Public'] } else { 'n/a' }
				LatestHotFix= if ($hotfix) { '{0} {1}' -f $hotfix.HotFixID,$hotfix.InstalledOn.ToShortDateString() } else { 'n/a' }
				PendingReboot=[bool]$pending
				GpuSummary= if ($gpus.Count) { [string]($gpus -join '; ') } else { 'n/a' }
				ThermalSummary= if ($thermal.Count) { [string]($thermal -join '; ') } else { 'n/a' }
			} | ConvertTo-Json -Compress -Depth 3
			""";
		var dto = DeserializeList<SecurityDto>(RunPowerShell(script, 25_000)).FirstOrDefault();
		return dto is null
			? SecuritySnapshot.Empty
			: new SecuritySnapshot(dto.DefenderRealTime ?? "n/a", dto.DefenderAntivirus ?? "n/a", dto.DefenderSignatureAgeDays, dto.FirewallDomain ?? "n/a", dto.FirewallPrivate ?? "n/a", dto.FirewallPublic ?? "n/a", dto.LatestHotFix ?? "n/a", dto.PendingReboot, dto.GpuSummary ?? "n/a", dto.ThermalSummary ?? "n/a");
	}

	public static IReadOnlyList<ScheduledTaskSnapshot> ReadScheduledTasks()
	{
		var script = """
			$ErrorActionPreference = 'SilentlyContinue'
			@(Get-ScheduledTask | ForEach-Object {
				$info = Get-ScheduledTaskInfo -TaskName $_.TaskName -TaskPath $_.TaskPath -ErrorAction SilentlyContinue
				[pscustomobject]@{
					Id=($_.TaskPath + '|' + $_.TaskName)
					TaskPath=[string]$_.TaskPath
					TaskName=[string]$_.TaskName
					State=[string]$_.State
					Enabled=([string]$_.State -ne 'Disabled')
					LastRunTime= if ($info -and $info.LastRunTime) { $info.LastRunTime.ToString('o') } else { $null }
					NextRunTime= if ($info -and $info.NextRunTime) { $info.NextRunTime.ToString('o') } else { $null }
					Action=[string](($_.Actions | ForEach-Object { ($_.Execute + ' ' + $_.Arguments).Trim() }) -join '; ')
				}
			}) | ConvertTo-Json -Compress -Depth 3
			""";
		return DeserializeList<ScheduledTaskDto>(RunPowerShell(script, 30_000))
			.Select(t => new ScheduledTaskSnapshot(t.Id ?? $"{t.TaskPath}|{t.TaskName}", t.TaskPath ?? "", t.TaskName ?? "", t.State ?? "n/a", t.Enabled, t.LastRunTime, t.NextRunTime, t.Action ?? ""))
			.ToList();
	}

	public static IReadOnlyList<FirewallRuleSnapshot> ReadFirewallRules()
	{
		var script = """
			$ErrorActionPreference = 'SilentlyContinue'
			@(Get-NetFirewallRule |
				Select-Object `
					@{Name='Name';Expression={[string]$_.Name}},
					@{Name='DisplayName';Expression={[string]$_.DisplayName}},
					@{Name='Enabled';Expression={$_.Enabled -eq 'True'}},
					@{Name='Direction';Expression={[string]$_.Direction}},
					@{Name='Action';Expression={[string]$_.Action}},
					@{Name='Profile';Expression={[string]$_.Profile}},
					@{Name='Group';Expression={[string]$_.Group}}) | ConvertTo-Json -Compress -Depth 3
			""";
		return DeserializeList<FirewallRuleDto>(RunPowerShell(script, 30_000))
			.Select(r => new FirewallRuleSnapshot(r.Name ?? "", string.IsNullOrWhiteSpace(r.DisplayName) ? r.Name ?? "n/a" : r.DisplayName, r.Enabled, r.Direction ?? "", r.Action ?? "", r.Profile ?? "", r.Group ?? ""))
			.Where(r => !string.IsNullOrWhiteSpace(r.Name))
			.ToList();
	}

	public static IReadOnlyList<LocalUserSnapshot> ReadLocalUsers()
	{
		var script = """
			$ErrorActionPreference = 'SilentlyContinue'
			$adminNames = @{}
			foreach ($m in @(Get-LocalGroupMember -Group 'Administrators')) { $adminNames[$m.Name] = $true; $adminNames[($m.Name -split '\\')[-1]] = $true }
			$rows = @()
			foreach ($u in @(Get-LocalUser)) {
				$rows += [pscustomobject]@{
					Kind='User'
					Name=[string]$u.Name
					Enabled=[bool]$u.Enabled
					IsAdmin=($adminNames.ContainsKey($u.Name))
					LastLogon= if ($u.LastLogon) { $u.LastLogon.ToString('o') } else { $null }
					Description=[string]$u.Description
					Members=''
				}
			}
			foreach ($g in @(Get-LocalGroup)) {
				$members = @(Get-LocalGroupMember -Group $g.Name -ErrorAction SilentlyContinue | ForEach-Object Name)
				$rows += [pscustomobject]@{
					Kind='Group'
					Name=[string]$g.Name
					Enabled=$true
					IsAdmin=($g.Name -eq 'Administrators')
					LastLogon=$null
					Description=[string]$g.Description
					Members=[string]($members -join '; ')
				}
			}
			@($rows | Sort-Object Kind,Name) | ConvertTo-Json -Compress -Depth 3
			""";
		return DeserializeList<LocalUserDto>(RunPowerShell(script, 20_000))
			.Select(u => new LocalUserSnapshot(u.Kind ?? "", u.Name ?? "", u.Enabled, u.IsAdmin, u.LastLogon, u.Description, u.Members ?? ""))
			.Where(u => !string.IsNullOrWhiteSpace(u.Name))
			.ToList();
	}

	internal static string RunPowerShell(string script, int timeoutMs = 12_000)
	{
		using var process = Process.Start(new ProcessStartInfo("powershell.exe")
		{
			RedirectStandardOutput = true,
			RedirectStandardError = true,
			UseShellExecute = false,
			CreateNoWindow = true
		}.WithArguments("-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script));
		if (process is null)
		{
			throw new InvalidOperationException("powershell.exe could not be started");
		}

		var stdoutTask = process.StandardOutput.ReadToEndAsync();
		var stderrTask = process.StandardError.ReadToEndAsync();
		if (!process.WaitForExit(timeoutMs))
		{
			try
			{
				process.Kill(entireProcessTree: true);
			}
			catch
			{
				// Best effort cleanup.
			}
			throw new InvalidOperationException("PowerShell command timed out");
		}

		Task.WaitAll(stdoutTask, stderrTask);
		var stdout = stdoutTask.Result;
		var stderr = stderrTask.Result;
		if (process.ExitCode != 0)
		{
			throw new InvalidOperationException(LimitMessage(string.IsNullOrWhiteSpace(stderr) ? stdout : stderr));
		}

		return stdout;
	}

	private static List<T> DeserializeList<T>(string json)
	{
		if (string.IsNullOrWhiteSpace(json))
		{
			return [];
		}

		var trimmed = json.Trim();
		if (trimmed == "null")
		{
			return [];
		}

		if (trimmed.StartsWith("[", StringComparison.Ordinal))
		{
			return JsonSerializer.Deserialize<List<T>>(trimmed, JsonOptions) ?? [];
		}

		var single = JsonSerializer.Deserialize<T>(trimmed, JsonOptions);
		return single is null ? [] : [single];
	}

	private static string LimitMessage(string value)
	{
		value = value.ReplaceLineEndings(" ").Trim();
		return value.Length <= 180 ? value : value[..180] + "...";
	}

	private sealed class UsbDeviceDto
	{
		public string? Name { get; set; }
		public string? Status { get; set; }
		public string? Class { get; set; }
		public string? InstanceId { get; set; }
		public bool Present { get; set; }
	}

	private sealed class UsbDriverDto
	{
		public string? DeviceName { get; set; }
		public string? Provider { get; set; }
		public string? Version { get; set; }
		public string? InfName { get; set; }
		public string? DeviceClass { get; set; }
		public string? DeviceId { get; set; }
	}

	private sealed class ServiceDto
	{
		public string? Name { get; set; }
		public string? DisplayName { get; set; }
		public string? State { get; set; }
		public string? StartMode { get; set; }
		public bool AcceptStop { get; set; }
		public int ProcessId { get; set; }
		public string? DependsOn { get; set; }
		public string? DependedOnBy { get; set; }
		public string? Path { get; set; }
	}

	private sealed class NetworkConnectionDto
	{
		public string? Protocol { get; set; }
		public string? State { get; set; }
		public string? LocalEndpoint { get; set; }
		public string? RemoteEndpoint { get; set; }
		public int ProcessId { get; set; }
		public string? ProcessName { get; set; }
	}

	private sealed class StartupItemDto
	{
		public string? Name { get; set; }
		public string? Source { get; set; }
		public string? Location { get; set; }
		public string? Command { get; set; }
	}

	private sealed class EventDto
	{
		public DateTime? TimeCreated { get; set; }
		public string? LogName { get; set; }
		public string? Level { get; set; }
		public string? Provider { get; set; }
		public int Id { get; set; }
		public string? Message { get; set; }
	}

	private sealed class DriverDto
	{
		public string? Name { get; set; }
		public string? DisplayName { get; set; }
		public string? State { get; set; }
		public string? StartMode { get; set; }
		public string? Path { get; set; }
	}

	private sealed class SecurityDto
	{
		public string? DefenderRealTime { get; set; }
		public string? DefenderAntivirus { get; set; }
		public int DefenderSignatureAgeDays { get; set; }
		public string? FirewallDomain { get; set; }
		public string? FirewallPrivate { get; set; }
		public string? FirewallPublic { get; set; }
		public string? LatestHotFix { get; set; }
		public bool PendingReboot { get; set; }
		public string? GpuSummary { get; set; }
		public string? ThermalSummary { get; set; }
	}

	private sealed class ScheduledTaskDto
	{
		public string? Id { get; set; }
		public string? TaskPath { get; set; }
		public string? TaskName { get; set; }
		public string? State { get; set; }
		public bool Enabled { get; set; }
		public DateTime? LastRunTime { get; set; }
		public DateTime? NextRunTime { get; set; }
		public string? Action { get; set; }
	}

	private sealed class FirewallRuleDto
	{
		public string? Name { get; set; }
		public string? DisplayName { get; set; }
		public bool Enabled { get; set; }
		public string? Direction { get; set; }
		public string? Action { get; set; }
		public string? Profile { get; set; }
		public string? Group { get; set; }
	}

	private sealed class LocalUserDto
	{
		public string? Kind { get; set; }
		public string? Name { get; set; }
		public bool Enabled { get; set; }
		public bool IsAdmin { get; set; }
		public DateTime? LastLogon { get; set; }
		public string? Description { get; set; }
		public string? Members { get; set; }
	}
}

internal static class WindowsActions
{
	public static void DisablePnpDevice(string instanceId)
	{
		WindowsInventory.RunPowerShell($"$ErrorActionPreference='Stop'; Disable-PnpDevice -InstanceId {PsQuote(instanceId)} -Confirm:$false | Out-Null", 20_000);
	}

	public static void EnablePnpDevice(string instanceId)
	{
		WindowsInventory.RunPowerShell($"$ErrorActionPreference='Stop'; Enable-PnpDevice -InstanceId {PsQuote(instanceId)} -Confirm:$false | Out-Null", 20_000);
	}

	public static void StopService(string serviceName)
	{
		WindowsInventory.RunPowerShell($"$ErrorActionPreference='Stop'; Stop-Service -Name {PsQuote(serviceName)} -Force -ErrorAction Stop", 20_000);
	}

	public static void StartService(string serviceName)
	{
		WindowsInventory.RunPowerShell($"$ErrorActionPreference='Stop'; Start-Service -Name {PsQuote(serviceName)} -ErrorAction Stop", 20_000);
	}

	public static void RestartService(string serviceName)
	{
		WindowsInventory.RunPowerShell($"$ErrorActionPreference='Stop'; Restart-Service -Name {PsQuote(serviceName)} -Force -ErrorAction Stop", 30_000);
	}

	public static void SetProcessPriority(int pid, string priority)
	{
		using var process = Process.GetProcessById(pid);
		process.PriorityClass = Enum.Parse<ProcessPriorityClass>(priority, ignoreCase: true);
	}

	public static void KillProcessTree(int pid)
	{
		using var process = Process.GetProcessById(pid);
		process.Kill(entireProcessTree: true);
	}

	public static void SuspendProcess(int pid)
	{
		NativeMethods.SuspendProcess(pid);
	}

	public static void ResumeProcess(int pid)
	{
		NativeMethods.ResumeProcess(pid);
	}

	public static void EnableScheduledTask(string id)
	{
		var (path, name) = SplitTaskId(id);
		WindowsInventory.RunPowerShell($"$ErrorActionPreference='Stop'; Enable-ScheduledTask -TaskPath {PsQuote(path)} -TaskName {PsQuote(name)} | Out-Null", 20_000);
	}

	public static void DisableScheduledTask(string id)
	{
		var (path, name) = SplitTaskId(id);
		WindowsInventory.RunPowerShell($"$ErrorActionPreference='Stop'; Disable-ScheduledTask -TaskPath {PsQuote(path)} -TaskName {PsQuote(name)} | Out-Null", 20_000);
	}

	public static void StartScheduledTask(string id)
	{
		var (path, name) = SplitTaskId(id);
		WindowsInventory.RunPowerShell($"$ErrorActionPreference='Stop'; Start-ScheduledTask -TaskPath {PsQuote(path)} -TaskName {PsQuote(name)}", 20_000);
	}

	public static void SetFirewallRule(string name, bool enabled)
	{
		WindowsInventory.RunPowerShell($"$ErrorActionPreference='Stop'; Set-NetFirewallRule -Name {PsQuote(name)} -Enabled {(enabled ? "True" : "False")}", 20_000);
	}

	private static string PsQuote(string value) => "'" + value.Replace("'", "''") + "'";

	private static (string Path, string Name) SplitTaskId(string id)
	{
		var index = id.IndexOf('|', StringComparison.Ordinal);
		return index < 0 ? ("\\", id) : (id[..index], id[(index + 1)..]);
	}
}

internal static class ProcessStartInfoExtensions
{
	public static ProcessStartInfo WithArguments(this ProcessStartInfo startInfo, params string[] arguments)
	{
		foreach (var argument in arguments)
		{
			startInfo.ArgumentList.Add(argument);
		}
		return startInfo;
	}
}

internal static class NativeMethods
{
	private const int SystemProcessorPerformanceInformation = 8;
	private const uint ProcessSuspendResume = 0x0800;

	[DllImport("ntdll.dll")]
	private static extern int NtQuerySystemInformation(int systemInformationClass, IntPtr systemInformation, int systemInformationLength, out int returnLength);

	[DllImport("kernel32.dll", SetLastError = true)]
	internal static extern bool GlobalMemoryStatusEx(ref MemoryStatusEx buffer);

	[DllImport("kernel32.dll", SetLastError = true)]
	internal static extern bool GetSystemPowerStatus(out SystemPowerStatus powerStatus);

	[DllImport("kernel32.dll", SetLastError = true)]
	private static extern IntPtr CreateToolhelp32Snapshot(uint flags, uint processId);

	[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
	private static extern bool Process32FirstW(IntPtr snapshot, ref ProcessEntry32 entry);

	[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
	private static extern bool Process32NextW(IntPtr snapshot, ref ProcessEntry32 entry);

	[DllImport("kernel32.dll", SetLastError = true)]
	private static extern bool CloseHandle(IntPtr handle);

	[DllImport("kernel32.dll", SetLastError = true)]
	private static extern IntPtr OpenProcess(uint desiredAccess, bool inheritHandle, int processId);

	[DllImport("ntdll.dll")]
	private static extern int NtSuspendProcess(IntPtr processHandle);

	[DllImport("ntdll.dll")]
	private static extern int NtResumeProcess(IntPtr processHandle);

	public static void SuspendProcess(int pid)
	{
		WithProcessHandle(pid, handle =>
		{
			var status = NtSuspendProcess(handle);
			if (status != 0)
			{
				throw new InvalidOperationException($"NtSuspendProcess failed with status 0x{status:X}");
			}
		});
	}

	public static void ResumeProcess(int pid)
	{
		WithProcessHandle(pid, handle =>
		{
			var status = NtResumeProcess(handle);
			if (status != 0)
			{
				throw new InvalidOperationException($"NtResumeProcess failed with status 0x{status:X}");
			}
		});
	}

	private static void WithProcessHandle(int pid, Action<IntPtr> action)
	{
		var handle = OpenProcess(ProcessSuspendResume, inheritHandle: false, pid);
		if (handle == IntPtr.Zero)
		{
			throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
		}

		try
		{
			action(handle);
		}
		finally
		{
			CloseHandle(handle);
		}
	}

	public static Dictionary<int, int> ReadParentProcessIds()
	{
		const uint snapshotProcesses = 0x00000002;
		var handle = CreateToolhelp32Snapshot(snapshotProcesses, 0);
		if (handle == new IntPtr(-1))
		{
			return [];
		}

		try
		{
			var result = new Dictionary<int, int>();
			var entry = new ProcessEntry32 { Size = (uint)Marshal.SizeOf<ProcessEntry32>() };
			if (!Process32FirstW(handle, ref entry))
			{
				return result;
			}

			do
			{
				result[(int)entry.ProcessId] = (int)entry.ParentProcessId;
				entry.Size = (uint)Marshal.SizeOf<ProcessEntry32>();
			}
			while (Process32NextW(handle, ref entry));
			return result;
		}
		finally
		{
			CloseHandle(handle);
		}
	}

	public static ProcessorTimes[] ReadProcessorTimes()
	{
		var count = Math.Max(1, Environment.ProcessorCount);
		var size = Marshal.SizeOf<ProcessorPerformanceInformation>();
		var length = size * count;
		var ptr = Marshal.AllocHGlobal(length);
		try
		{
			var status = NtQuerySystemInformation(SystemProcessorPerformanceInformation, ptr, length, out var returned);
			if (status != 0 || returned <= 0)
			{
				return new ProcessorTimes[count];
			}

			var actualCount = Math.Max(1, Math.Min(count, returned / size));
			var values = new ProcessorTimes[actualCount];
			for (var i = 0; i < actualCount; i++)
			{
				var item = Marshal.PtrToStructure<ProcessorPerformanceInformation>(ptr + i * size);
				values[i] = new ProcessorTimes(item.IdleTime, item.KernelTime + item.UserTime, item.KernelTime + item.UserTime - item.IdleTime);
			}

			return values;
		}
		finally
		{
			Marshal.FreeHGlobal(ptr);
		}
	}

	[StructLayout(LayoutKind.Sequential)]
	private struct ProcessorPerformanceInformation
	{
		public long IdleTime;
		public long KernelTime;
		public long UserTime;
		public long DpcTime;
		public long InterruptTime;
		public uint InterruptCount;
	}

	[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
	internal struct MemoryStatusEx
	{
		public uint Length;
		public uint MemoryLoad;
		public ulong TotalPhys;
		public ulong AvailPhys;
		public ulong TotalPageFile;
		public ulong AvailPageFile;
		public ulong TotalVirtual;
		public ulong AvailVirtual;
		public ulong AvailExtendedVirtual;
	}

	[StructLayout(LayoutKind.Sequential)]
	internal struct SystemPowerStatus
	{
		public byte AcLineStatus;
		public byte BatteryFlag;
		public byte BatteryLifePercent;
		public byte SystemStatusFlag;
		public int BatteryLifeTime;
		public int BatteryFullLifeTime;
	}

	[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
	private struct ProcessEntry32
	{
		public uint Size;
		public uint Usage;
		public uint ProcessId;
		public IntPtr DefaultHeapId;
		public uint ModuleId;
		public uint Threads;
		public uint ParentProcessId;
		public int PriorityBase;
		public uint Flags;

		[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
		public string ExecutableFile;
	}
}

internal readonly record struct ProcessorTimes(long Idle, long Total, long Busy);
internal enum ViewMode
{
	Monitor,
	Usb,
	Services,
	Network,
	Startup,
	Events,
	Drivers,
	Security,
	Tasks,
	Firewall,
	Users
}

internal enum PendingActionKind
{
	KillProcess,
	DisableDevice,
	EnableDevice,
	StopService,
	RestartService,
	StartService,
	SetProcessPriority,
	KillProcessTree,
	SuspendProcess,
	ResumeProcess,
	EnableTask,
	DisableTask,
	StartTask,
	EnableFirewallRule,
	DisableFirewallRule
}

internal readonly record struct PendingAction(PendingActionKind Kind, string Id, string Name);
internal enum LogLevel
{
	Info,
	Warn,
	Error
}

internal readonly record struct LogEntry(DateTime Time, LogLevel Level, string Message);
internal readonly record struct HealthScore(int Score, string Label);
internal readonly record struct SystemSnapshot(string MachineName, string OsDescription, TimeSpan Uptime, int? BatteryPercent, bool? OnAcPower);
internal readonly record struct CpuSnapshot(double TotalPercent, double[] CorePercents, IReadOnlyList<double> History);
internal readonly record struct MemorySnapshot(ulong TotalBytes, ulong UsedBytes, ulong AvailableBytes, ulong PageFileTotalBytes, ulong PageFileUsedBytes, double UsedPercent, IReadOnlyList<double> History);
internal readonly record struct AdapterSnapshot(string Name, double DownloadBytesPerSec, double UploadBytesPerSec);
internal readonly record struct NetworkSnapshot(double DownloadBytesPerSec, double UploadBytesPerSec, long TotalDownloadedBytes, long TotalUploadedBytes, IReadOnlyList<AdapterSnapshot> Adapters, IReadOnlyList<double> History, double HistoryMax);
internal readonly record struct DiskSnapshot(string Name, string Format, long TotalBytes, long UsedBytes, long FreeBytes, double UsedPercent);
internal readonly record struct ProcessSnapshot(int Pid, int ParentPid, string Name, double CpuPercent, long MemoryBytes, int ThreadCount, int HandleCount, string Priority, bool Responding, DateTime? StartTime, string? Path, int NetworkConnectionCount, int EstablishedConnectionCount, IReadOnlyList<double> CpuHistory, IReadOnlyList<double> MemoryHistory);
internal readonly record struct UsbDeviceSnapshot(string Name, string Status, string Class, string InstanceId, bool Present);
internal readonly record struct UsbDriverSnapshot(string DeviceName, string Provider, string Version, string InfName, string DeviceClass, string DeviceId);
internal readonly record struct ServiceSnapshot(string Name, string DisplayName, string State, string StartMode, bool AcceptStop, int ProcessId, string DependsOn, string DependedOnBy, string? Path);
internal readonly record struct NetworkConnectionSnapshot(string Protocol, string State, string LocalEndpoint, string RemoteEndpoint, int ProcessId, string ProcessName);
internal readonly record struct StartupItemSnapshot(string Name, string Source, string Location, string Command);
internal readonly record struct EventSnapshot(DateTime? TimeCreated, string LogName, string Level, string Provider, int Id, string Message);
internal readonly record struct DriverSnapshot(string Name, string DisplayName, string State, string StartMode, string? Path);
internal readonly record struct ScheduledTaskSnapshot(string Id, string TaskPath, string TaskName, string State, bool Enabled, DateTime? LastRunTime, DateTime? NextRunTime, string Action);
internal readonly record struct FirewallRuleSnapshot(string Name, string DisplayName, bool Enabled, string Direction, string Action, string Profile, string Group);
internal readonly record struct LocalUserSnapshot(string Kind, string Name, bool Enabled, bool IsAdmin, DateTime? LastLogon, string? Description, string Members);
internal readonly record struct SecuritySnapshot(string DefenderRealTime, string DefenderAntivirus, int DefenderSignatureAgeDays, string FirewallDomain, string FirewallPrivate, string FirewallPublic, string LatestHotFix, bool PendingReboot, string GpuSummary, string ThermalSummary)
{
	public static SecuritySnapshot Empty { get; } = new("n/a", "n/a", -1, "n/a", "n/a", "n/a", "n/a", false, "n/a", "n/a");

	public string ToDisplayText() => $"Defender RT {DefenderRealTime}; AV {DefenderAntivirus}; FW D/P/P {FirewallDomain}/{FirewallPrivate}/{FirewallPublic}; LatestHotFix {LatestHotFix}; PendingReboot {PendingReboot}; GPU {GpuSummary}; Thermal {ThermalSummary}";
}
internal readonly record struct Snapshot(DateTime Time, SystemSnapshot System, CpuSnapshot Cpu, MemorySnapshot Memory, NetworkSnapshot Network, IReadOnlyList<DiskSnapshot> Disks, IReadOnlyList<ProcessSnapshot> Processes, IReadOnlyList<UsbDeviceSnapshot> UsbDevices, IReadOnlyList<UsbDriverSnapshot> UsbDrivers, IReadOnlyList<ServiceSnapshot> Services, IReadOnlyList<NetworkConnectionSnapshot> NetworkConnections, IReadOnlyList<StartupItemSnapshot> StartupItems, IReadOnlyList<EventSnapshot> Events, IReadOnlyList<DriverSnapshot> Drivers, IReadOnlyList<ScheduledTaskSnapshot> ScheduledTasks, IReadOnlyList<FirewallRuleSnapshot> FirewallRules, IReadOnlyList<LocalUserSnapshot> LocalUsers, SecuritySnapshot Security)
{
	public static Snapshot Empty { get; } = new(
		DateTime.Now,
		new SystemSnapshot(Environment.MachineName, RuntimeInformation.OSDescription.Trim(), TimeSpan.Zero, null, null),
		new CpuSnapshot(0, [], []),
		new MemorySnapshot(0, 0, 0, 0, 0, 0, []),
		new NetworkSnapshot(0, 0, 0, 0, [], [], 1),
		[],
		[],
		[],
		[],
		[],
		[],
		[],
		[],
		[],
		[],
		[],
		[],
		SecuritySnapshot.Empty);
}

internal sealed class Canvas
{
	private readonly int width;
	private readonly int height;
	private readonly Cell[,] cells;

	public Canvas(int width, int height)
	{
		this.width = width;
		this.height = height;
		cells = new Cell[height, width];
		for (var row = 0; row < height; row++)
		{
			for (var col = 0; col < width; col++)
			{
				cells[row, col] = new Cell(' ', Theme.Reset);
			}
		}
	}

	public void Box(int x, int y, int w, int h, string title)
	{
		if (w < 2 || h < 2)
		{
			return;
		}

		var left = x - 1;
		var top = y - 1;
		var right = Math.Min(width - 1, left + w - 1);
		var bottom = Math.Min(height - 1, top + h - 1);
		var color = Theme.Border;

		Set(top, left, '╭', color);
		Set(top, right, '╮', color);
		Set(bottom, left, '╰', color);
		Set(bottom, right, '╯', color);
		for (var col = left + 1; col < right; col++)
		{
			Set(top, col, '─', color);
			Set(bottom, col, '─', color);
		}
		for (var row = top + 1; row < bottom; row++)
		{
			Set(row, left, '│', color);
			Set(row, right, '│', color);
		}

		Text(y, x + 2, Theme.Title, $" {title} ");
	}

	public void Text(int y, int x, string color, string text)
	{
		var row = y - 1;
		var col = x - 1;
		if (row < 0 || row >= height || col >= width)
		{
			return;
		}

		foreach (var ch in text)
		{
			if (col >= width)
			{
				break;
			}
			if (col >= 0)
			{
				Set(row, col, ch, color);
			}
			col++;
		}
	}

	public string Render()
	{
		var sb = new StringBuilder(width * height + height * 24);
		sb.Append("\x1b[H");
		string? currentColor = null;
		for (var row = 0; row < height; row++)
		{
			for (var col = 0; col < width; col++)
			{
				var cell = cells[row, col];
				if (cell.Color != currentColor)
				{
					sb.Append(cell.Color);
					currentColor = cell.Color;
				}
				sb.Append(cell.Ch);
			}
			if (row < height - 1)
			{
				sb.Append('\n');
			}
		}
		sb.Append(Theme.Reset);
		return sb.ToString();
	}

	private void Set(int row, int col, char ch, string color)
	{
		if (row < 0 || row >= height || col < 0 || col >= width)
		{
			return;
		}
		cells[row, col] = new Cell(ch, color);
	}

	private readonly record struct Cell(char Ch, string Color);
}

internal static class Graph
{
	private static readonly char[] Blocks = [' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];

	public static string Line(IReadOnlyList<double> values, int width, double max)
	{
		if (width <= 0)
		{
			return string.Empty;
		}
		if (values.Count == 0)
		{
			return new string(' ', width);
		}

		max = Math.Max(1, max);
		var start = Math.Max(0, values.Count - width);
		var sb = new StringBuilder(width);
		for (var i = start; i < values.Count; i++)
		{
			var level = (int)Math.Round(Math.Clamp(values[i] / max, 0, 1) * (Blocks.Length - 1));
			sb.Append(Blocks[level]);
		}
		if (sb.Length < width)
		{
			sb.Insert(0, new string(' ', width - sb.Length));
		}
		return sb.ToString();
	}
}

internal static class Text
{
	public static string Fit(string value, int width)
	{
		if (width <= 0)
		{
			return string.Empty;
		}
		return value.Length <= width ? value : value[..Math.Max(0, width - 1)] + "…";
	}
}

internal static class Units
{
	public const double Gib = 1024.0 * 1024.0 * 1024.0;

	public static string Size(long bytes)
	{
		return Size((double)bytes);
	}

	public static string Size(ulong bytes)
	{
		return Size((double)bytes);
	}

	public static string Rate(double bytesPerSecond)
	{
		return $"{Size(bytesPerSecond)}/s";
	}

	public static string Duration(TimeSpan value)
	{
		if (value.TotalDays >= 1)
		{
			return $"{(int)value.TotalDays}d {value.Hours:00}h {value.Minutes:00}m";
		}
		return $"{value.Hours:00}h {value.Minutes:00}m {value.Seconds:00}s";
	}

	private static string Size(double bytes)
	{
		string[] units = ["B", "KiB", "MiB", "GiB", "TiB"];
		var value = bytes;
		var unit = 0;
		while (value >= 1024 && unit < units.Length - 1)
		{
			value /= 1024;
			unit++;
		}
		return $"{value:0.0} {units[unit]}";
	}
}

internal static class Color
{
	public static string Percent(double percent)
	{
		return percent switch
		{
			>= 90 => Theme.Error,
			>= 70 => Theme.Warn,
			>= 40 => Theme.Highlight,
			_ => Theme.Ok
		};
	}
}

internal static class Theme
{
	public const string Reset = "\x1b[0m";
	public const string Bg = "\x1b[48;2;14;18;28m";
	public const string TransBlue = "\x1b[38;2;91;206;250m";
	public const string TransPink = "\x1b[38;2;245;169;184m";
	public const string TransWhite = "\x1b[38;2;255;255;255m";
	public const string Border = "\x1b[38;2;91;206;250m";
	public const string Title = "\x1b[1;38;2;245;169;184m";
	public const string Highlight = "\x1b[38;2;91;206;250m";
	public const string SoftText = "\x1b[38;2;205;219;236m";
	public const string Muted = "\x1b[38;2;255;255;255m";
	public const string Cpu = "\x1b[38;2;245;169;184m";
	public const string Memory = "\x1b[38;2;255;255;255m";
	public const string Net = "\x1b[38;2;91;206;250m";
	public const string Ok = "\x1b[38;2;147;232;178m";
	public const string Warn = "\x1b[38;2;255;219;128m";
	public const string Error = "\x1b[38;2;255;118;150m";
	public const string Selected = "\x1b[1;38;2;255;255;255m\x1b[48;2;76;48;72m";
}

internal static class Terminal
{
	private const int StdOutputHandle = -11;
	private const uint EnableVirtualTerminalProcessing = 0x0004;

	[DllImport("kernel32.dll", SetLastError = true)]
	private static extern IntPtr GetStdHandle(int nStdHandle);

	[DllImport("kernel32.dll", SetLastError = true)]
	private static extern bool GetConsoleMode(IntPtr hConsoleHandle, out uint lpMode);

	[DllImport("kernel32.dll", SetLastError = true)]
	private static extern bool SetConsoleMode(IntPtr hConsoleHandle, uint dwMode);

	public static void EnableVirtualTerminal()
	{
		var handle = GetStdHandle(StdOutputHandle);
		if (handle == IntPtr.Zero || !GetConsoleMode(handle, out var mode))
		{
			return;
		}
		SetConsoleMode(handle, mode | EnableVirtualTerminalProcessing);
	}

	public static void Write(string value)
	{
		Console.Write(value);
	}
}
