hematite-cli 0.8.2

Senior SysAdmin, Network Admin, Data Analyst, and Software Engineer living in your terminal. A high-precision local AI agent harness for LM Studio, Ollama, and other local OpenAI-compatible runtimes that runs 100% on your own silicon. Reads repos, edits files, runs builds, inspects full network state and workstation telemetry, and runs real Python/JS for data analysis.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
use std::fmt::Write as _;

pub struct Recipe {
    pub severity: &'static str,
    pub title: &'static str,
    pub steps: &'static [&'static str],
    pub dig_deeper: Option<&'static str>,
}

struct RecipeAc {
    ac: aho_corasick::AhoCorasick,
    recipe_indices: Vec<usize>,
}

static RECIPE_AC: std::sync::OnceLock<RecipeAc> = std::sync::OnceLock::new();

fn recipe_ac() -> &'static RecipeAc {
    RECIPE_AC.get_or_init(|| {
        let total: usize = ALL_RECIPES.iter().map(|e| e.triggers.len()).sum();
        let mut patterns: Vec<&str> = Vec::with_capacity(total);
        let mut recipe_indices: Vec<usize> = Vec::with_capacity(total);
        for (i, entry) in ALL_RECIPES.iter().enumerate() {
            for &trigger in entry.triggers {
                patterns.push(trigger);
                recipe_indices.push(i);
            }
        }
        RecipeAc {
            ac: aho_corasick::AhoCorasick::new(&patterns).expect("valid patterns"),
            recipe_indices,
        }
    })
}

pub fn match_recipes(output: &str) -> Vec<&'static Recipe> {
    let lower = output.to_ascii_lowercase();
    let state = recipe_ac();
    let mut seen = std::collections::HashSet::new();
    let mut matches: Vec<&'static Recipe> = Vec::new();
    for mat in state.ac.find_iter(&lower) {
        let idx = state.recipe_indices[mat.pattern().as_usize()];
        if seen.insert(idx) {
            matches.push(&ALL_RECIPES[idx].recipe);
        }
    }
    matches
}

fn collect_unique_recipes(outputs: &[(&str, &str)]) -> Vec<&'static Recipe> {
    let mut all_recipes: Vec<&'static Recipe> = Vec::new();
    let mut seen_titles = std::collections::HashSet::new();
    for (_label, output) in outputs {
        for recipe in match_recipes(output) {
            if seen_titles.insert(recipe.title) {
                all_recipes.push(recipe);
            }
        }
    }
    all_recipes
}

struct RecipeEntry {
    triggers: &'static [&'static str],
    recipe: Recipe,
}

static ALL_RECIPES: &[RecipeEntry] = &[
    // ── Disk / Storage ────────────────────────────────────────────────────────
    RecipeEntry {
        triggers: &["very low", "disk:", "free space"],
        recipe: Recipe {
            severity: "ACTION",
            title: "Low disk space",
            steps: &[
                "Open Disk Cleanup: press Win+R → type 'cleanmgr' → select C: → check all boxes including 'Windows Update Cleanup'",
                "Empty the Recycle Bin: right-click desktop icon → Empty Recycle Bin",
                "Clear Temp folder: press Win+R → type '%temp%' → Ctrl+A → Delete (skip files in use)",
                "Check largest folders: open PowerShell → Get-ChildItem C:\\ -Recurse -ErrorAction SilentlyContinue | Sort-Object Length -Descending | Select-Object -First 20 FullName, Length",
                "If space is still tight, run: winget install -e --id Microsoft.PowerToys then use PowerToys Disk Space Analyzer",
            ],
            dig_deeper: Some("storage"),
        },
    },
    RecipeEntry {
        triggers: &["disk_health", "smart", "predictive failure", "wear"],
        recipe: Recipe {
            severity: "ACTION",
            title: "Drive health warning — possible failure",
            steps: &[
                "Back up your important files immediately before doing anything else",
                "Verify the SMART status: open PowerShell (admin) → Get-PhysicalDisk | Select FriendlyName, HealthStatus, OperationalStatus",
                "If HealthStatus is 'Unhealthy' or 'Warning', replace the drive — do not wait",
                "For SSDs: check manufacturer's NVMe/SSD tool (Samsung Magician, Crucial Storage Executive, etc.) for wear level",
            ],
            dig_deeper: Some("disk_health"),
        },
    },

    // ── Reboot ────────────────────────────────────────────────────────────────
    RecipeEntry {
        triggers: &["pending reboot", "restart when convenient", "reboot required"],
        recipe: Recipe {
            severity: "INVESTIGATE",
            title: "Restart required",
            steps: &[
                "Save your work and restart the computer — pending file operations and updates cannot apply until you do",
                "After restarting, run this report again to confirm the reboot flag cleared",
                "If the flag persists after a restart, check Windows Update: Settings → Windows Update → View update history → look for stuck installs",
            ],
            dig_deeper: Some("pending_reboot"),
        },
    },

    // ── Event log errors ──────────────────────────────────────────────────────
    RecipeEntry {
        triggers: &["critical/error event", "error events in windows event log", "critical error"],
        recipe: Recipe {
            severity: "INVESTIGATE",
            title: "Windows event log errors detected",
            steps: &[
                "Find the top error sources: PowerShell → Get-WinEvent -FilterHashtable @{LogName='System','Application';Level=1,2} -MaxEvents 100 | Group-Object ProviderName | Sort-Object Count -Descending | Select -First 10",
                "One crashing service or driver usually causes most of the noise — focus on the source with the highest count",
                "For 'Service Control Manager' errors: check which service is crashing → Get-WinEvent -FilterHashtable @{LogName='System';ProviderName='Service Control Manager';Level=2} -MaxEvents 10 | Select Message",
                "For application crashes: check AppEvent for the faulting app name → Get-WinEvent -FilterHashtable @{LogName='Application';Level=2} -MaxEvents 10 | Select TimeCreated,Message",
            ],
            dig_deeper: Some("log_check"),
        },
    },

    // ── Services ──────────────────────────────────────────────────────────────
    RecipeEntry {
        triggers: &["critical service", "not running: windefend", "not running: eventlog", "not running: dnscache"],
        recipe: Recipe {
            severity: "ACTION",
            title: "Critical Windows service not running",
            steps: &[
                "Open Services: press Win+R → type 'services.msc' → Enter",
                "Find the stopped service, right-click → Start",
                "If it fails to start, right-click → Properties → Recovery tab → set 'First failure' to 'Restart the Service'",
                "For Windows Defender (WinDefend) stopped: open Windows Security → Virus & threat protection → turn on Real-time protection",
                "If EventLog is stopped, restart is required — this service cannot be started manually once stopped",
            ],
            dig_deeper: Some("services"),
        },
    },

    // ── Network ───────────────────────────────────────────────────────────────
    RecipeEntry {
        triggers: &["internet connectivity: unreachable", "could not ping 1.1.1.1"],
        recipe: Recipe {
            severity: "ACTION",
            title: "No internet connectivity",
            steps: &[
                "Check physical connection: is the Ethernet cable plugged in, or is Wi-Fi connected?",
                "Test gateway reachability: PowerShell → Test-Connection (Get-NetRoute -DestinationPrefix '0.0.0.0/0').NextHop -Count 1",
                "Flush DNS cache: PowerShell (admin) → Clear-DnsClientCache",
                "Reset TCP/IP stack: PowerShell (admin) → netsh int ip reset; netsh winsock reset → then restart",
                "If on Wi-Fi: forget the network and reconnect, or try 'netsh wlan disconnect' then 'netsh wlan connect name=\"SSID\"'",
            ],
            dig_deeper: Some("connectivity"),
        },
    },
    RecipeEntry {
        triggers: &["high latency", "ms rtt — high latency"],
        recipe: Recipe {
            severity: "MONITOR",
            title: "High network latency detected",
            steps: &[
                "Run a traceroute to find where the delay is: PowerShell → tracert 1.1.1.1",
                "Check for background bandwidth consumers: Task Manager → Performance → Open Resource Monitor → Network tab",
                "If on Wi-Fi, check signal strength and try moving closer to the router or switching to 5GHz",
                "Check your ISP's status page for outages in your area",
            ],
            dig_deeper: Some("latency"),
        },
    },

    // ── RAM ───────────────────────────────────────────────────────────────────
    RecipeEntry {
        triggers: &["ram:", "very low", "running a bit low", "free of"],
        recipe: Recipe {
            severity: "MONITOR",
            title: "High memory usage",
            steps: &[
                "Find the top RAM consumers: Task Manager → Memory column (sort descending)",
                "Close unused browser tabs — each tab can consume 100–500 MB",
                "Check for memory leaks: if one process is growing over time without release, restart it",
                "Disable startup programs that aren't needed: Task Manager → Startup tab → disable high-impact items",
                "If consistently above 85% with normal usage, consider adding RAM",
            ],
            dig_deeper: Some("resource_load"),
        },
    },

    // ── Thermal ───────────────────────────────────────────────────────────────
    RecipeEntry {
        triggers: &["very high", "check cooling", "elevated under load", "°c — very high"],
        recipe: Recipe {
            severity: "ACTION",
            title: "CPU running hot",
            steps: &[
                "Shut down and clean dust from fans and heatsink with compressed air — this is the fix 90% of the time",
                "Check that all fan headers are connected and fans are spinning on boot",
                "Verify thermal paste on CPU heatsink — if it's more than 4 years old and temperatures are high, repaste",
                "In BIOS: confirm fan curve is not set to 'Silent' mode — switch to 'Standard' or 'Performance'",
                "Check for CPU throttling: PowerShell → Get-WmiObject -Class Win32_Processor | Select Name,CurrentClockSpeed,MaxClockSpeed — if Current is much lower than Max under load, it's throttling",
            ],
            dig_deeper: Some("thermal"),
        },
    },

    // ── Security ──────────────────────────────────────────────────────────────
    RecipeEntry {
        triggers: &["real-time protection: disabled", "defender.*disabled", "firewall.*off"],
        recipe: Recipe {
            severity: "ACTION",
            title: "Windows security protection disabled",
            steps: &[
                "Re-enable Defender real-time protection: Windows Security → Virus & threat protection → turn on Real-time protection",
                "If Defender shows as disabled by a third-party antivirus, ensure that AV is up to date and its own real-time protection is on",
                "Re-enable Windows Firewall: Control Panel → Windows Defender Firewall → Turn Windows Defender Firewall on or off → turn on for all profiles",
                "Run a quick scan: Windows Security → Virus & threat protection → Quick scan",
            ],
            dig_deeper: Some("security"),
        },
    },
    RecipeEntry {
        triggers: &["threat detected", "quarantine", "malware", "virus found"],
        recipe: Recipe {
            severity: "ACTION",
            title: "Threat detected by Windows Defender",
            steps: &[
                "Open Windows Security → Virus & threat protection → Protection history → review detected threats",
                "If action is 'Quarantined', Defender has contained it — review and remove from quarantine",
                "Run a full offline scan: Windows Security → Virus & threat protection → Scan options → Microsoft Defender Offline scan",
                "Change passwords for any accounts accessed on this machine after the infection date",
                "Check browser extensions for anything you didn't install",
            ],
            dig_deeper: Some("defender_quarantine"),
        },
    },

    // ── Windows Update ────────────────────────────────────────────────────────
    RecipeEntry {
        triggers: &["windows update", "pending update", "update.*required"],
        recipe: Recipe {
            severity: "INVESTIGATE",
            title: "Windows updates pending",
            steps: &[
                "Open Settings → Windows Update → Check for updates",
                "Install all available updates, then restart when prompted",
                "If updates are stuck: PowerShell (admin) → net stop wuauserv; net stop bits; net start wuauserv; net start bits",
                "If stuck for more than 24 hours: run the Windows Update Troubleshooter from Settings → System → Troubleshoot → Other troubleshooters",
            ],
            dig_deeper: Some("updates"),
        },
    },

    // ── Device / driver errors ────────────────────────────────────────────────
    RecipeEntry {
        triggers: &["yellow bang", "pnp error", "configmanager error", "error code 43", "error code 10", "error code 28", "device problem", "driver error"],
        recipe: Recipe {
            severity: "ACTION",
            title: "Hardware device error detected",
            steps: &[
                "Open Device Manager: press Win+R → type 'devmgmt.msc' → Enter",
                "Look for yellow exclamation marks (!) — right-click → Properties → note the error code and device name",
                "Error Code 43 (USB/GPU): unplug and replug the device, or roll back the driver: right-click → Properties → Driver → Roll Back Driver",
                "Error Code 10 (failed to start): update the driver — right-click → Update driver → Search automatically",
                "Error Code 28 (no driver): download the driver from the manufacturer's website (look up the device name + Windows version)",
                "For recurring errors: run SFC scan → PowerShell (admin) → sfc /scannow",
            ],
            dig_deeper: Some("device_health"),
        },
    },

    // ── No backup configured ──────────────────────────────────────────────────
    RecipeEntry {
        triggers: &["file history: disabled", "no backup configured", "no restore points", "last backup: never", "backup: not configured", "file history.*disabled", "no system restore"],
        recipe: Recipe {
            severity: "INVESTIGATE",
            title: "No backup configured",
            steps: &[
                "Enable File History: Settings → System → Storage → Advanced storage settings → Backup options → Add a drive",
                "Enable System Restore: search 'Create a restore point' → select C: → Configure → turn on protection → OK → Create",
                "For a full image backup: search 'Backup and Restore (Windows 7)' → Create a system image → choose an external drive",
                "OneDrive Known Folder Backup covers Desktop/Documents/Pictures: Settings → OneDrive → Backup → Manage backup",
                "Run your first backup immediately — a backup that has never run has zero value",
            ],
            dig_deeper: Some("windows_backup"),
        },
    },

    // ── SMB1 enabled ─────────────────────────────────────────────────────────
    RecipeEntry {
        triggers: &["smb1 is enabled", "smb1: enabled", "smb1 protocol: enabled", "smb version 1", "smbv1 enabled"],
        recipe: Recipe {
            severity: "ACTION",
            title: "SMB1 protocol enabled — security risk",
            steps: &[
                "SMB1 is a deprecated protocol exploited by WannaCry and NotPetya ransomware — disable it immediately",
                "Disable SMB1: PowerShell (admin) → Set-SmbServerConfiguration -EnableSMB1Protocol $false -Force",
                "Verify it's off: PowerShell → Get-SmbServerConfiguration | Select EnableSMB1Protocol (should show False)",
                "If a legacy device (old NAS, printer) stops working after disabling, upgrade its firmware or replace it — do not re-enable SMB1",
                "Restart required to fully remove the SMB1 listener",
            ],
            dig_deeper: Some("shares"),
        },
    },

    // ── BitLocker not protecting ──────────────────────────────────────────────
    RecipeEntry {
        triggers: &["protection state: off", "bitlocker: off", "bitlocker.*not protecting", "encryption status: fully decrypted", "bitlocker.*disabled"],
        recipe: Recipe {
            severity: "MONITOR",
            title: "Drive encryption not enabled",
            steps: &[
                "BitLocker encrypts your drive so data is unreadable if the laptop is lost or stolen — strongly recommended on portable machines",
                "Enable BitLocker: search 'Manage BitLocker' → Turn on BitLocker for C: → follow the wizard",
                "Save the recovery key to your Microsoft account or print it — you will need it if Windows can't auto-unlock at boot",
                "Encryption runs in the background and takes 1–3 hours for a typical drive — the PC remains usable during this time",
                "Requires TPM 1.2+ or USB key; check: PowerShell → Get-Tpm | Select TpmPresent,TpmReady",
            ],
            dig_deeper: Some("bitlocker"),
        },
    },

    // ── DNS resolution failing ────────────────────────────────────────────────
    RecipeEntry {
        triggers: &["dns resolution: failed", "dns: failed", "dns fail", "dns resolution failed", "could not resolve"],
        recipe: Recipe {
            severity: "ACTION",
            title: "DNS resolution failing",
            steps: &[
                "Flush DNS cache: PowerShell (admin) → Clear-DnsClientCache",
                "Test DNS directly: PowerShell → Resolve-DnsName google.com -Server 8.8.8.8 — if this works, your DNS server is the problem",
                "Switch to a reliable DNS server: PowerShell (admin) → Set-DnsClientServerAddress -InterfaceAlias 'Wi-Fi' -ServerAddresses ('8.8.8.8','1.1.1.1')",
                "Check if the DNS client service is running: Get-Service Dnscache | Select Status",
                "If on a corporate network or VPN, contact IT — split DNS may require the VPN to be connected for internal names to resolve",
            ],
            dig_deeper: Some("dns_servers"),
        },
    },

    // ── Repeated app crashes ──────────────────────────────────────────────────
    RecipeEntry {
        triggers: &["faulting application", "crash count", "crash frequency", "application hang", "faulting module"],
        recipe: Recipe {
            severity: "INVESTIGATE",
            title: "Application crashing repeatedly",
            steps: &[
                "Note the faulting application name and module from the report — these are the most important clues",
                "If the faulting module is ntdll.dll or a system DLL: run SFC to repair Windows files → PowerShell (admin) → sfc /scannow",
                "If the faulting module is a third-party DLL (e.g. a codec or plugin): uninstall the associated program",
                "Update or reinstall the crashing application — corrupted installs are a common cause",
                "Check for conflicting software: antivirus, screen recorders, and overlays (Discord, GeForce Experience) frequently inject into other processes",
                "If it is a Microsoft Office app: run the Office repair → Control Panel → Programs → right-click Office → Change → Quick Repair",
            ],
            dig_deeper: Some("app_crashes"),
        },
    },

    // ── Visual C++ / runtime missing ─────────────────────────────────────────
    RecipeEntry {
        triggers: &["vcruntime", "msvcr", "0xc000007b", "side-by-side configuration", "missing runtime", "vc++ redistributable"],
        recipe: Recipe {
            severity: "ACTION",
            title: "Visual C++ runtime missing or corrupt",
            steps: &[
                "Download and install the latest Visual C++ Redistributable packages (both x64 and x86) from Microsoft: search 'Visual C++ Redistributable downloads'",
                "Install all available years: 2015–2022 package covers most apps; older apps may need 2013, 2012, or 2010 separately",
                "If a specific app shows error 0xc000007b: right-click the app → Properties → Compatibility → Run as administrator",
                "Repair existing runtimes: Control Panel → Programs → find 'Microsoft Visual C++ 20XX' → Repair",
                "After installing, restart before testing the application again — runtimes must be registered at boot",
            ],
            dig_deeper: None,
        },
    },

    // ── Certificate expiring ──────────────────────────────────────────────────
    RecipeEntry {
        triggers: &["expiring within 30 days", "expires in", "certificate expir", "cert.*expir"],
        recipe: Recipe {
            severity: "INVESTIGATE",
            title: "Certificate expiring soon",
            steps: &[
                "Open Certificate Manager: press Win+R → type 'certmgr.msc' → check Personal → Certificates for the expiring cert",
                "Note the certificate subject and issuer — determines who you need to contact for renewal",
                "For personal/S-MIME certificates: renew through your CA or email provider portal",
                "For web/TLS certificates on a server: generate a new CSR and submit to your CA before expiry",
                "For code-signing certificates: do not let these lapse — signed binaries will show 'unknown publisher' warnings after expiry",
            ],
            dig_deeper: Some("certificates"),
        },
    },

    // ── Wi-Fi weak signal ─────────────────────────────────────────────────────
    RecipeEntry {
        triggers: &["signal: poor", "weak signal", "rssi: -8", "rssi: -9", "signal strength: poor", "quality: poor", "poor signal"],
        recipe: Recipe {
            severity: "MONITOR",
            title: "Wi-Fi signal weak",
            steps: &[
                "Move closer to the router or access point — Wi-Fi degrades quickly through walls and floors",
                "Switch to 5 GHz band if available — faster and less congested in most home environments (but shorter range than 2.4 GHz)",
                "Check for interference: microwave ovens, baby monitors, and neighboring networks on the same channel all degrade signal",
                "Change the router's Wi-Fi channel: log into router admin → Wireless settings → try channels 1, 6, or 11 (2.4 GHz) or auto (5 GHz)",
                "Update the Wi-Fi adapter driver: Device Manager → Network Adapters → right-click adapter → Update driver",
                "If signal is consistently poor from a fixed desk, consider a powerline adapter or mesh Wi-Fi node nearby",
            ],
            dig_deeper: Some("wifi"),
        },
    },

    // ── NTP / time sync failure ───────────────────────────────────────────────
    RecipeEntry {
        triggers: &["time sync failed", "sync failed", "clock drift", "ntp.*error", "w32tm.*fail", "ntp source.*unreachable", "time.*not synchronized"],
        recipe: Recipe {
            severity: "INVESTIGATE",
            title: "System clock not synchronizing",
            steps: &[
                "Force a sync now: PowerShell (admin) → w32tm /resync /force",
                "Check the current NTP source: PowerShell → w32tm /query /source",
                "If source shows 'Local CMOS Clock' or 'Free-running', the time service has lost its server",
                "Reset to Microsoft's NTP server: PowerShell (admin) → w32tm /config /manualpeerlist:time.windows.com /syncfromflags:manual /reliable:YES /update",
                "Restart the time service: PowerShell (admin) → Restart-Service w32tm",
                "If clock drift is large (>5 minutes), some authentication systems (Kerberos, MFA) will fail until synced",
            ],
            dig_deeper: Some("ntp"),
        },
    },

    // ── Page file missing ─────────────────────────────────────────────────────
    RecipeEntry {
        triggers: &["no page file", "pagefile: none", "page file: none", "virtual memory: none", "pagefile not configured", "no pagefile"],
        recipe: Recipe {
            severity: "INVESTIGATE",
            title: "Page file not configured",
            steps: &[
                "Windows needs a page file even with plenty of RAM — some apps and crash dumps require it",
                "Re-enable automatic page file management: search 'Adjust the appearance and performance of Windows' → Advanced → Virtual memory → Change → check 'Automatically manage'",
                "If manually set: assign at least 1.5× your RAM as maximum size on the system drive",
                "After changing page file settings, restart is required — changes do not take effect until reboot",
                "Note: if this machine intentionally has no page file (e.g. a RAM disk setup), verify that was deliberate before changing it",
            ],
            dig_deeper: Some("pagefile"),
        },
    },

    // ── System file corruption ────────────────────────────────────────────────
    RecipeEntry {
        triggers: &["corrupt files found", "autorepairrequired: true", "integrity.*failed", "component store corruption", "sfc.*corrupt", "windows resource protection found corrupt"],
        recipe: Recipe {
            severity: "ACTION",
            title: "Windows system file corruption detected",
            steps: &[
                "Run SFC to repair corrupt files: PowerShell (admin) → sfc /scannow (takes 5–15 minutes)",
                "If SFC reports 'Windows Resource Protection found corrupt files but was unable to fix some of them', run DISM next:",
                "DISM repair: PowerShell (admin) → DISM /Online /Cleanup-Image /RestoreHealth (requires internet access, 10–30 minutes)",
                "Run SFC again after DISM completes — DISM provides the source files SFC needs",
                "Restart after both complete, then check Event Viewer for CBS log: Applications and Services Logs → Microsoft → Windows → Servicing",
                "If corruption persists after both tools: in-place upgrade repair (Windows Setup without wiping data) is the next step",
            ],
            dig_deeper: Some("integrity"),
        },
    },

    // ── Service start failure ─────────────────────────────────────────────────
    RecipeEntry {
        triggers: &["stopped unexpectedly", "failed to start", "error 1067", "error 1053", "service terminated", "exited with code", "failed to respond"],
        recipe: Recipe {
            severity: "INVESTIGATE",
            title: "Service failed to start or stopped unexpectedly",
            steps: &[
                "Find the failing service name in the report, then check its status: PowerShell → Get-Service <ServiceName>",
                "Read the specific error from the Application/System event log: Event Viewer → Windows Logs → System → filter for Service Control Manager (Event ID 7034 or 7031)",
                "Try to start it manually: PowerShell (admin) → Start-Service <ServiceName> — note any error message",
                "Check if the service account has the right permissions: Services console (services.msc) → right-click → Properties → Log On tab",
                "Look for a dependent service that failed first — a service won't start if something it requires is stopped",
                "If the service EXE is missing or corrupt, reinstall the application that owns it",
            ],
            dig_deeper: Some("services"),
        },
    },

    // ── RDP unreachable ───────────────────────────────────────────────────────
    RecipeEntry {
        triggers: &["fdenytsconnections: 1", "no enabled rdp firewall", "rdp status: disabled"],
        recipe: Recipe {
            severity: "ACTION",
            title: "Remote Desktop (RDP) is disabled or blocked",
            steps: &[
                "Enable RDP: Settings → System → Remote Desktop → Enable Remote Desktop (or PowerShell admin: Set-ItemProperty 'HKLM:\\System\\CurrentControlSet\\Control\\Terminal Server' fDenyTSConnections 0)",
                "Ensure the RDP firewall rule is enabled: PowerShell (admin) → Enable-NetFirewallRule -DisplayGroup 'Remote Desktop'",
                "Verify port 3389 is listening after enabling: PowerShell → netstat -an | findstr 3389",
                "If NLA is required, make sure the connecting user account has the right to log in remotely (must be in Remote Desktop Users group or Administrators)",
                "Check that Windows Firewall is not blocking the connection — on the host, temporarily allow pings to confirm network path is open",
                "For cloud VMs: check the security group / NSG allows inbound TCP 3389 from your IP",
            ],
            dig_deeper: Some("rdp"),
        },
    },

    // ── Windows Update service broken ─────────────────────────────────────────
    RecipeEntry {
        triggers: &["wuauserv: stopped", "wuauserv stopped", "windows update: stopped", "update service stopped", "bits: stopped", "bits stopped"],
        recipe: Recipe {
            severity: "ACTION",
            title: "Windows Update service is stopped or broken",
            steps: &[
                "Run the Windows Update Troubleshooter: Settings → Update & Security → Troubleshoot → Additional troubleshooters → Windows Update",
                "Manually restart the update services: PowerShell (admin) → Stop-Service wuauserv, bits, cryptsvc, msiserver → Start-Service wuauserv, bits, cryptsvc",
                "Clear the update cache if stuck: PowerShell (admin) → Stop-Service wuauserv → Remove-Item C:\\Windows\\SoftwareDistribution\\* -Recurse -Force → Start-Service wuauserv",
                "Check for conflicting 3rd-party update tools (WSUS, SCCM, Intune policies) that may be disabling updates",
                "Run the System Update Readiness Tool: DISM /Online /Cleanup-Image /RestoreHealth",
                "If the service keeps stopping, check Event Viewer → Windows Logs → System for Windows Update Agent errors around the same time",
            ],
            dig_deeper: Some("updates"),
        },
    },

    // ── Teams cache ───────────────────────────────────────────────────────────
    RecipeEntry {
        triggers: &["classic teams cache:", "new teams cache:", "msteams cache:", "teams cache size"],
        recipe: Recipe {
            severity: "INVESTIGATE",
            title: "Teams cache — clear to resolve most Teams issues",
            steps: &[
                "Quit Teams completely: right-click the Teams icon in the system tray → Quit",
                "Clear Classic Teams cache: open Run (Win+R) → type %AppData%\\Microsoft\\Teams → delete the contents of: Cache, blob_storage, databases, GPUCache, IndexedDB, Local Storage, tmp",
                "Clear New Teams (MSTeams) cache: open Run → %LocalAppData%\\Packages\\MSTeams_8wekyb3d8bbwe\\LocalCache\\ → delete all contents",
                "Restart Teams and sign in — cache rebuilds from the server automatically",
                "If Teams still fails to sign in after clearing cache, also clear credentials: Credential Manager (Win+R → credmgr.msc) → Windows Credentials → remove all MicrosoftOffice16_Data:SSPI:* entries",
            ],
            dig_deeper: Some("teams"),
        },
    },

    // ── M365 token broker not running ─────────────────────────────────────────
    RecipeEntry {
        triggers: &["token broker: not running", "aad broker plugin: not found", "web account manager: not running", "wam: not running", "aad broker: not found"],
        recipe: Recipe {
            severity: "ACTION",
            title: "Microsoft 365 authentication broker not running",
            steps: &[
                "The Windows Account Manager (WAM) and AAD Broker are required for M365 sign-in — if they're not running, Teams, Outlook, and OneDrive will loop on sign-in",
                "Re-register the token broker: PowerShell (admin) → sfc /scannow — this repairs the system files WAM depends on",
                "Restart the TokenBroker service: PowerShell (admin) → Restart-Service TokenBroker -ErrorAction SilentlyContinue",
                "If re-registering doesn't help, sign out of all work accounts: Settings → Accounts → Access work or school → disconnect and reconnect your org account",
                "On Intune/AAD-joined machines: run 'dsregcmd /leave' then 'dsregcmd /join' (admin) to re-register the device — requires network connectivity to Azure AD",
                "Check for conflicting credential entries: Credential Manager → Windows Credentials → remove stale MicrosoftOffice16_Data:SSPI:* and MicrosoftOffice15_Data:* entries",
            ],
            dig_deeper: Some("identity_auth"),
        },
    },

    // ── WMI repository corrupt ────────────────────────────────────────────────
    RecipeEntry {
        triggers: &["wmi repository is inconsistent", "repository is inconsistent", "wmi: inconsistent", "verifyrepository: inconsistent", "wmi corruption"],
        recipe: Recipe {
            severity: "ACTION",
            title: "WMI repository corrupt — cascading tool failures",
            steps: &[
                "WMI corruption breaks PowerShell Get-WmiObject, Defender, Windows Update, and many admin tools — fix it first before investigating other issues",
                "Stop WMI: PowerShell (admin) → net stop winmgmt /y",
                "Rebuild the repository: PowerShell (admin) → winmgmt /resetrepository",
                "Start WMI: PowerShell (admin) → net start winmgmt",
                "Verify the fix: PowerShell → winmgmt /verifyrepository — should say 'WMI repository is consistent'",
                "If resetrepository fails, try salvage mode: winmgmt /salvagerepository — this preserves customizations",
                "Restart the machine after repair — WMI caches are session-scoped and some tools won't see the fix until reboot",
            ],
            dig_deeper: Some("wmi_health"),
        },
    },

    // ── Windows not activated ─────────────────────────────────────────────────
    RecipeEntry {
        triggers: &["license status: unlicensed", "license status: notification", "activation: not activated", "not genuine", "windows is not activated"],
        recipe: Recipe {
            severity: "INVESTIGATE",
            title: "Windows not activated",
            steps: &[
                "Check activation status: Settings → System → Activation — note the exact status message",
                "If you have a product key: Settings → System → Activation → Change product key → enter the 25-character key",
                "If the key was tied to a Microsoft account: sign in with that Microsoft account and activation should happen automatically over the internet",
                "Force activation attempt: PowerShell (admin) → slmgr /ato",
                "If you get error 0xC004F074 (Key Management Service unreachable): you're on a domain with KMS — contact your IT department, the KMS server may be offline",
                "If you recently changed hardware (motherboard): activation may need to be relinked — use the Activation Troubleshooter in Settings",
            ],
            dig_deeper: Some("activation"),
        },
    },

    // ── Windows Search not indexing ───────────────────────────────────────────
    RecipeEntry {
        triggers: &["wsearch: stopped", "search service: stopped", "wsearch service: stopped", "indexer: stopped", "windows search: stopped"],
        recipe: Recipe {
            severity: "INVESTIGATE",
            title: "Windows Search not running — search won't find files",
            steps: &[
                "Start the Windows Search service: PowerShell (admin) → Start-Service WSearch",
                "Set it to start automatically: PowerShell (admin) → Set-Service WSearch -StartupType Automatic",
                "If the service won't start: check Event Viewer → Windows Logs → Application → filter for 'Search' for the specific error",
                "Rebuild the search index: Settings → Privacy & Security → Windows Search → Advanced indexing options → Advanced → Rebuild — takes 15–60 minutes",
                "If rebuilding doesn't help, reset the index database: Stop-Service WSearch → delete C:\\ProgramData\\Microsoft\\Search\\Data\\Applications\\Windows\\Windows.edb → Start-Service WSearch",
                "Restart File Explorer after: PowerShell → Stop-Process -Name explorer → Start-Process explorer",
            ],
            dig_deeper: Some("search_index"),
        },
    },

    // ── OneDrive sync error ───────────────────────────────────────────────────
    RecipeEntry {
        triggers: &["sync status: error", "onedrive: not running", "sync errors detected", "onedrive sync error", "known folder backup: not configured"],
        recipe: Recipe {
            severity: "INVESTIGATE",
            title: "OneDrive not syncing",
            steps: &[
                "Check the sync status icon in the system tray — hover over it for the specific error message",
                "Common fix: right-click the OneDrive tray icon → Pause syncing → Resume syncing — resets stuck sync state",
                "If that doesn't work: right-click the OneDrive tray icon → Settings → Account → Unlink this PC → relink with the same account",
                "Check for conflicting files: File Explorer → OneDrive folder → look for files with a red X — rename or delete the local copy and let it sync from the cloud",
                "If the issue is 'Not enough space in OneDrive': manage storage at onedrive.live.com/manage",
                "Reset OneDrive if all else fails: Win+R → %localappdata%\\Microsoft\\OneDrive\\onedrive.exe /reset — wait 2 minutes, then reopen OneDrive from Start",
            ],
            dig_deeper: Some("onedrive"),
        },
    },

    // ── Printer offline or stuck queue ────────────────────────────────────────
    RecipeEntry {
        triggers: &["status: offline", "pending jobs:", "print spooler: stopped", "spooler: stopped"],
        recipe: Recipe {
            severity: "INVESTIGATE",
            title: "Printer offline or stuck print queue",
            steps: &[
                "Check the printer is powered on and connected (USB cable or same Wi-Fi network as the PC)",
                "Clear the stuck print queue: PowerShell (admin) → Stop-Service Spooler → Remove-Item C:\\Windows\\System32\\spool\\PRINTERS\\* -Force → Start-Service Spooler",
                "If printer shows Offline: right-click the printer in Settings → Printers & scanners → See what's printing → Printer menu → uncheck 'Use Printer Offline'",
                "For network printers: verify the printer's IP hasn't changed — print a configuration page from the printer itself to check its current IP",
                "Re-add the printer if the IP changed: Settings → Bluetooth & devices → Printers & scanners → Add device → Add manually → enter the new IP",
                "If the Print Spooler service is stopped: PowerShell (admin) → Start-Service Spooler → Set-Service Spooler -StartupType Automatic",
            ],
            dig_deeper: Some("printers"),
        },
    },

    // ── No Outlook mail profile ───────────────────────────────────────────────
    RecipeEntry {
        triggers: &["profile count: 0", "no mail profiles", "mail profile: none", "no profiles configured", "outlook profiles: 0"],
        recipe: Recipe {
            severity: "ACTION",
            title: "No Outlook mail profile — Outlook will not open",
            steps: &[
                "Outlook requires at least one mail profile to start — create one from the Mail control panel applet, not from within Outlook",
                "Open Mail applet: Win+R → type 'control mlcfg32.cpl' (or search 'Mail' in Control Panel) → Show Profiles → Add",
                "Enter a profile name (e.g. 'Outlook') → Add Account → enter your email address and follow the auto-configuration wizard",
                "For Exchange/Microsoft 365: the wizard needs network access to find the Autodiscover DNS record — ensure VPN is connected if this is a corporate account",
                "For manual setup: choose 'Manual setup' → Microsoft Exchange or compatible service → enter server and username from your IT department",
                "After creating the profile: launch Outlook, sign in if prompted — first launch will take 2–10 minutes to download the mailbox",
            ],
            dig_deeper: Some("outlook"),
        },
    },

    // ── PrintNightmare not mitigated ──────────────────────────────────────────
    RecipeEntry {
        triggers: &["rpcauthnlevelprivacyenabled: 0", "printnightmare rpc mitigation not applied", "point and print allows silent", "finding: printnightmare"],
        recipe: Recipe {
            severity: "INVESTIGATE",
            title: "PrintNightmare (CVE-2021-34527) mitigation not applied",
            steps: &[
                "Apply the RPC authentication hardening fix: PowerShell (admin) → Set-ItemProperty 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Print' -Name RpcAuthnLevelPrivacyEnabled -Value 1 -Type DWord",
                "Restrict Point and Print driver installs to administrators: PowerShell (admin) → Set-ItemProperty 'HKLM:\\SOFTWARE\\Policies\\Microsoft\\Windows NT\\Printers\\PointAndPrint' -Name RestrictDriverInstallationToAdministrators -Value 1 -Type DWord",
                "If the Print Spooler is not needed (e.g. server, workstation that never prints remotely): PowerShell (admin) → Stop-Service Spooler → Set-Service Spooler -StartupType Disabled",
                "Verify patch KB5004945 or later is installed: check Windows Update history for July 2021 security updates",
                "Restart the Spooler service after registry changes: PowerShell (admin) → Restart-Service Spooler",
            ],
            dig_deeper: Some("print_spooler"),
        },
    },
];

pub struct HealthScore {
    pub grade: char,
    pub label: &'static str,
    pub action_count: usize,
    pub investigate_count: usize,
    pub monitor_count: usize,
}

impl HealthScore {
    pub fn summary_line(&self) -> String {
        match (
            self.action_count,
            self.investigate_count,
            self.monitor_count,
        ) {
            (0, 0, 0) => "No issues found — machine is healthy.".to_string(),
            (0, 0, m) => format!("{} item(s) to monitor.", m),
            (0, i, 0) => format!("{} item(s) need investigation.", i),
            (0, i, m) => format!("{} item(s) need investigation, {} to monitor.", i, m),
            (a, 0, 0) => format!("{} item(s) require immediate action.", a),
            (a, i, _) => format!(
                "{} item(s) require immediate action, {} need investigation.",
                a, i
            ),
        }
    }

    /// A plain-English sentence for non-technical users that explains what
    /// the grade means and what to do next.
    pub fn grade_intro(&self) -> &'static str {
        match self.grade {
            'A' => "Your PC is in great shape — no issues were found. The diagnostic data below is included for reference.",
            'B' => "Your PC is doing well, but there's one thing worth a closer look. The action plan below has specific steps.",
            'C' => "Your PC needs some attention. A couple of things should be investigated — follow the action plan below.",
            'D' => "Your PC needs attention. There are issues that should be fixed — follow the action plan below.",
            _ => "Your PC has critical issues that need immediate attention. Work through the action plan below as soon as possible.",
        }
    }
}

/// Compute a health grade (A–F) from diagnostic output sections.
pub fn score_health(outputs: &[(&str, &str)]) -> HealthScore {
    let all_recipes = collect_unique_recipes(outputs);

    let action_count = all_recipes
        .iter()
        .filter(|r| r.severity == "ACTION")
        .count();
    let investigate_count = all_recipes
        .iter()
        .filter(|r| r.severity == "INVESTIGATE")
        .count();
    let monitor_count = all_recipes
        .iter()
        .filter(|r| r.severity == "MONITOR")
        .count();

    let (grade, label) = if action_count >= 3 {
        ('F', "Critical")
    } else if action_count >= 1 {
        ('D', "Poor")
    } else if investigate_count >= 2 {
        ('C', "Fair")
    } else if investigate_count >= 1 {
        ('B', "Good")
    } else {
        ('A', "Excellent")
    };

    HealthScore {
        grade,
        label,
        action_count,
        investigate_count,
        monitor_count,
    }
}

/// Format all matching recipes for a given diagnostic output into a
/// human-readable action plan section suitable for a Markdown report.
pub fn format_action_plan(outputs: &[(&str, &str)]) -> String {
    let mut all_recipes = collect_unique_recipes(outputs);

    if all_recipes.is_empty() {
        return "No actionable findings — machine appears healthy.\n".to_string();
    }

    // Sort: ACTION first, then INVESTIGATE, then MONITOR
    all_recipes.sort_by_key(|r| match r.severity {
        "ACTION" => 0,
        "INVESTIGATE" => 1,
        _ => 2,
    });

    let mut out = String::with_capacity(all_recipes.len() * 200);
    for (i, recipe) in all_recipes.iter().enumerate() {
        let badge = match recipe.severity {
            "ACTION" => "⚠ ACTION REQUIRED",
            "INVESTIGATE" => "🔍 INVESTIGATE",
            _ => "📊 MONITOR",
        };
        let _ = write!(out, "### {}. {}{}\n\n", i + 1, badge, recipe.title);
        for step in recipe.steps {
            out.push_str("- ");
            out.push_str(step);
            out.push('\n');
        }
        if let Some(_topic) = recipe.dig_deeper {
            out.push_str("\n*Run `hematite --diagnose` for a deeper automated investigation.*\n");
        }
        out.push('\n');
    }

    out
}

/// Format all matching recipes as an HTML fragment for embedding in a report page.
pub fn format_action_plan_html(outputs: &[(&str, &str)]) -> String {
    let mut all_recipes = collect_unique_recipes(outputs);

    if all_recipes.is_empty() {
        return "<p class=\"healthy\">No actionable findings — machine appears healthy.</p>\n"
            .to_string();
    }

    all_recipes.sort_by_key(|r| match r.severity {
        "ACTION" => 0,
        "INVESTIGATE" => 1,
        _ => 2,
    });

    let mut out = String::with_capacity(all_recipes.len() * 400);
    for (i, recipe) in all_recipes.iter().enumerate() {
        let (sev_class, badge_class, badge_text) = match recipe.severity {
            "ACTION" => ("sev-action", "b-action", "ACTION REQUIRED"),
            "INVESTIGATE" => ("sev-investigate", "b-investigate", "INVESTIGATE"),
            _ => ("sev-monitor", "b-monitor", "MONITOR"),
        };
        let _ = writeln!(out, "<div class=\"recipe {}\">", sev_class);
        let _ = writeln!(
            out,
            "<h3><span class=\"badge {}\">{}</span> {}. {}</h3>",
            badge_class,
            badge_text,
            i + 1,
            he(recipe.title)
        );
        out.push_str("<ol>\n");
        for step in recipe.steps {
            let _ = writeln!(out, "<li>{}</li>", he(step));
        }
        out.push_str("</ol>\n");
        if let Some(_topic) = recipe.dig_deeper {
            out.push_str(
                "<p class=\"dig-deeper\">Run <code>hematite --diagnose</code> for a deeper automated investigation of this issue.</p>\n"
            );
        }
        out.push_str("</div>\n");
    }
    out
}

use crate::agent::html_template::he;