Skip to main content

hematite/agent/
report_export.rs

1use serde_json::json;
2use std::fmt::Write as _;
3use std::path::{Path, PathBuf};
4
5const REPORT_TOPICS: &[(&str, &str)] = &[
6    ("health_report", "System Health"),
7    ("hardware", "Hardware"),
8    ("storage", "Storage"),
9    ("network", "Network"),
10    ("security", "Security"),
11    ("toolchains", "Developer Toolchains"),
12];
13
14pub fn report_topics() -> &'static [(&'static str, &'static str)] {
15    REPORT_TOPICS
16}
17
18/// IT-first-look triage topics (health, security, connectivity, identity, updates).
19const TRIAGE_TOPICS: &[(&str, &str)] = &[
20    ("health_report", "System Health"),
21    ("security", "Security Posture"),
22    ("connectivity", "Connectivity"),
23    ("identity_auth", "Identity & Auth (M365/AAD)"),
24    ("updates", "Windows Updates"),
25];
26
27pub fn triage_topics_for_preset(preset: &str) -> &'static [(&'static str, &'static str)] {
28    match preset {
29        "network" => &[
30            ("connectivity", "Connectivity"),
31            ("wifi", "Wi-Fi"),
32            ("latency", "Latency"),
33            ("dns_servers", "DNS Servers"),
34            ("vpn", "VPN"),
35            ("proxy", "Proxy"),
36            ("connections", "Active Connections"),
37        ],
38        "security" => &[
39            ("security", "Security Posture"),
40            ("bitlocker", "BitLocker"),
41            ("tpm", "TPM / Secure Boot"),
42            ("local_security_policy", "Local Security Policy"),
43            ("shares", "SMB Shares"),
44            ("print_spooler", "Print Spooler"),
45        ],
46        "performance" => &[
47            ("resource_load", "Resource Load"),
48            ("thermal", "Thermal"),
49            ("cpu_power", "CPU Power"),
50            ("processes", "Top Processes"),
51            ("pagefile", "Page File"),
52            ("startup_items", "Startup Items"),
53        ],
54        "storage" => &[
55            ("storage", "Storage"),
56            ("disk_health", "Disk Health"),
57            ("shadow_copies", "Shadow Copies"),
58            ("storage_spaces", "Storage Spaces"),
59            ("bitlocker", "BitLocker"),
60        ],
61        "apps" => &[
62            ("browser_health", "Browser Health"),
63            ("outlook", "Outlook"),
64            ("teams", "Teams"),
65            ("installer_health", "Installer Health"),
66            ("onedrive", "OneDrive"),
67        ],
68        _ => TRIAGE_TOPICS,
69    }
70}
71
72fn triage_preset_title(preset: &str) -> &'static str {
73    match preset {
74        "network" => "Hematite Network Triage Report",
75        "security" => "Hematite Security Triage Report",
76        "performance" => "Hematite Performance Triage Report",
77        "storage" => "Hematite Storage Triage Report",
78        "apps" => "Hematite App Health Triage Report",
79        _ => "Hematite IT Triage Report",
80    }
81}
82
83/// Map a plain-English issue description to the most relevant inspect_host topics.
84fn topics_for_issue(issue: &str) -> Vec<(&'static str, &'static str)> {
85    let lower = issue.to_ascii_lowercase();
86    let mut seen = std::collections::HashSet::new();
87    let mut topics: Vec<(&'static str, &'static str)> = Vec::new();
88
89    macro_rules! add_if {
90        ($keywords:expr, $pairs:expr) => {
91            if $keywords.iter().any(|k: &&str| lower.contains(k)) {
92                for &pair in $pairs {
93                    if seen.insert(pair.0) {
94                        topics.push(pair);
95                    }
96                }
97            }
98        };
99    }
100
101    add_if!(
102        &[
103            "slow",
104            "lag",
105            "freeze",
106            "hang",
107            "sluggish",
108            "unresponsive",
109            "performance",
110            "high cpu",
111            "high ram",
112            "high memory",
113            "locking up"
114        ],
115        &[
116            ("resource_load", "Resource Load"),
117            ("thermal", "Thermal"),
118            ("cpu_power", "CPU Power"),
119            ("pagefile", "Page File"),
120            ("startup_items", "Startup Items")
121        ]
122    );
123    add_if!(
124        &[
125            "internet",
126            "network",
127            "wifi",
128            "wi-fi",
129            "wireless",
130            "offline",
131            "no web",
132            "can't browse",
133            "ping fails",
134            "no connection",
135            "can't connect"
136        ],
137        &[
138            ("connectivity", "Connectivity"),
139            ("wifi", "Wi-Fi"),
140            ("latency", "Latency"),
141            ("dns_servers", "DNS Servers")
142        ]
143    );
144    add_if!(
145        &["dns ", "dns:", "name resolution", "can't resolve"],
146        &[
147            ("dns_servers", "DNS Servers"),
148            ("connectivity", "Connectivity")
149        ]
150    );
151    add_if!(
152        &[
153            "vpn ",
154            "vpn:",
155            "tunnel",
156            "remote access",
157            "wireguard",
158            "anyconnect",
159            "globalprotect",
160            "pulse secure",
161            "split tunnel"
162        ],
163        &[
164            ("vpn", "VPN"),
165            ("connectivity", "Connectivity"),
166            ("proxy", "Proxy")
167        ]
168    );
169    add_if!(
170        &[
171            "disk full",
172            "out of space",
173            "low disk",
174            "disk space",
175            "drive full",
176            "drive almost full",
177            "c drive",
178            "storage full",
179            "no space",
180            "disk filling"
181        ],
182        &[
183            ("storage", "Storage"),
184            ("disk_health", "Disk Health"),
185            ("shadow_copies", "Shadow Copies")
186        ]
187    );
188    add_if!(
189        &[
190            "disk fail",
191            "drive fail",
192            "smart error",
193            "disk error",
194            "bad sector",
195            "drive health",
196            "disk health",
197            "check disk"
198        ],
199        &[("disk_health", "Disk Health"), ("storage", "Storage")]
200    );
201    add_if!(
202        &[
203            "slow boot",
204            "boot slow",
205            "slow startup",
206            "startup slow",
207            "takes forever to boot",
208            "not booting",
209            "won't boot",
210            "wont boot",
211            "boot fail",
212            "boot loop",
213            "stuck on spinning",
214            "spinning wheel",
215            "loading wheel",
216            "infinite loading"
217        ],
218        &[
219            ("startup_items", "Startup Items"),
220            ("services", "Services"),
221            ("disk_health", "Disk Health")
222        ]
223    );
224    add_if!(
225        &[
226            "crash",
227            "bsod",
228            "blue screen",
229            "unexpected restart",
230            "unexpected shutdown",
231            "kernel panic",
232            "stop error",
233            "restart loop",
234            "reboot loop",
235            "boot loop",
236            "stuck on restart",
237            "stuck restarting",
238            "endless reboot"
239        ],
240        &[
241            ("recent_crashes", "Crash History"),
242            ("log_check", "Event Log"),
243            ("thermal", "Thermal"),
244            ("disk_health", "Disk Health")
245        ]
246    );
247    add_if!(
248        &[
249            "app crash",
250            "application crash",
251            "program crash",
252            "program not opening",
253            "app not starting",
254            "not responding",
255            "application error"
256        ],
257        &[
258            ("app_crashes", "Application Crashes"),
259            ("log_check", "Event Log")
260        ]
261    );
262    add_if!(
263        &[
264            "update",
265            "windows update",
266            "patch",
267            "stuck on update",
268            "update fail",
269            "wuauserv"
270        ],
271        &[
272            ("updates", "Windows Updates"),
273            ("pending_reboot", "Pending Reboot"),
274            ("services", "Services")
275        ]
276    );
277    add_if!(
278        &[
279            "virus",
280            "malware",
281            "hacked",
282            "suspicious",
283            "threat",
284            "infected",
285            "ransomware",
286            "defender blocked",
287            "windows defender",
288            "defender flagged",
289            "defender quarantine"
290        ],
291        &[
292            ("security", "Security Posture"),
293            ("defender_quarantine", "Defender Quarantine"),
294            ("log_check", "Event Log")
295        ]
296    );
297    add_if!(
298        &[
299            "firewall",
300            "blocked port",
301            "blocked connection",
302            "port block"
303        ],
304        &[
305            ("security", "Security Posture"),
306            ("firewall_rules", "Firewall Rules")
307        ]
308    );
309    add_if!(
310        &[
311            "printer",
312            "printing",
313            "print queue",
314            "can't print",
315            "print fail"
316        ],
317        &[
318            ("printers", "Printers"),
319            ("print_spooler", "Print Spooler"),
320            ("drivers", "Drivers")
321        ]
322    );
323    add_if!(
324        &[
325            "sound",
326            "audio",
327            "speaker",
328            "no sound",
329            "headset",
330            "microphone",
331            "mic ",
332            "mic not",
333            "mic stopped",
334            "crackling",
335            "audio fail"
336        ],
337        &[("audio", "Audio"), ("drivers", "Drivers")]
338    );
339    add_if!(
340        &[
341            "bluetooth",
342            "headphones",
343            "airpods",
344            "wireless headset",
345            "bt "
346        ],
347        &[
348            ("bluetooth", "Bluetooth"),
349            ("audio", "Audio"),
350            ("device_health", "Device Health")
351        ]
352    );
353    add_if!(
354        &[
355            "camera",
356            "webcam",
357            "video call",
358            "camera not working",
359            "can't see camera"
360        ],
361        &[("camera", "Camera")]
362    );
363    add_if!(
364        &["teams", "microsoft teams"],
365        &[
366            ("teams", "Teams"),
367            ("identity_auth", "Identity & Auth"),
368            ("browser_health", "Browser Health"),
369            ("connectivity", "Connectivity")
370        ]
371    );
372    add_if!(
373        &["outlook", "email not working", "mail not", "calendar not"],
374        &[
375            ("outlook", "Outlook"),
376            ("identity_auth", "Identity & Auth"),
377            ("connectivity", "Connectivity")
378        ]
379    );
380    add_if!(
381        &[
382            "browser",
383            "chrome",
384            "edge ",
385            "firefox",
386            "slow browser",
387            "browser crash",
388            "browser not"
389        ],
390        &[("browser_health", "Browser Health")]
391    );
392    add_if!(
393        &[
394            "sign in",
395            "can't log in",
396            "login fail",
397            "password",
398            "pin not working",
399            "pin broken",
400            "pin stopped",
401            "hello pin",
402            "windows hello",
403            "fingerprint",
404            "hello not",
405            "locked out",
406            "authentication fail"
407        ],
408        &[
409            ("sign_in", "Sign-In / Windows Hello"),
410            ("identity_auth", "Identity & Auth"),
411            ("credentials", "Credentials")
412        ]
413    );
414    add_if!(
415        &[
416            "rdp",
417            "remote desktop",
418            "can't connect remotely",
419            "remote desktop not"
420        ],
421        &[
422            ("rdp", "Remote Desktop"),
423            ("connectivity", "Connectivity"),
424            ("firewall_rules", "Firewall Rules")
425        ]
426    );
427    add_if!(
428        &[
429            "device not recognized",
430            "driver not",
431            "usb not working",
432            "device problem",
433            "yellow bang",
434            "hardware not"
435        ],
436        &[
437            ("device_health", "Device Health"),
438            ("drivers", "Drivers"),
439            ("peripherals", "Peripherals")
440        ]
441    );
442    add_if!(
443        &[
444            "time wrong",
445            "clock wrong",
446            "wrong time",
447            "time sync",
448            "time off",
449            "time zone",
450            "timezone",
451            "wrong timezone",
452            "ntp ",
453            "ntp:",
454            "w32tm",
455            "w32time",
456            "windows time service",
457            "time service stopped"
458        ],
459        &[("ntp", "NTP / Time Sync")]
460    );
461    add_if!(
462        &[
463            "onedrive",
464            "one drive",
465            "file sync",
466            "not syncing",
467            "onedrive sync"
468        ],
469        &[("onedrive", "OneDrive")]
470    );
471    add_if!(
472        &["wmi error", "powershell wmi", "get-wmiobject fail"],
473        &[("wmi_health", "WMI Health")]
474    );
475    add_if!(
476        &[
477            "monitor",
478            "display",
479            "screen resolution",
480            "screen brightness",
481            "brightness not working",
482            "brightness stuck",
483            "second monitor",
484            "wrong resolution",
485            "display settings",
486            "refresh rate",
487            "scaling"
488        ],
489        &[("display_config", "Display Config")]
490    );
491    add_if!(
492        &[
493            "keyboard not",
494            "keyboard stop",
495            "keyboard broke",
496            "mouse not",
497            "mouse stop",
498            "mouse broke",
499            "touchpad",
500            "trackpad",
501            "peripheral not",
502            "numpad",
503            "special keys",
504            "fn key",
505            "function key",
506            "key not responding",
507            "keys not working"
508        ],
509        &[
510            ("peripherals", "Peripherals"),
511            ("device_health", "Device Health")
512        ]
513    );
514    add_if!(
515        &[
516            "hibernate",
517            "won't hibernate",
518            "wont hibernate",
519            "sleep issue",
520            "won't sleep",
521            "wont sleep",
522            "wont go to sleep",
523            "won't go to sleep",
524            "won't wake",
525            "wont wake",
526            "stuck after sleep",
527            "won't wake up",
528            "sleep mode"
529        ],
530        &[
531            ("pending_reboot", "Pending Reboot"),
532            ("services", "Services"),
533            ("thermal", "Thermal")
534        ]
535    );
536    add_if!(
537        &[
538            "microsoft store",
539            "store app",
540            "windows store",
541            "uwp",
542            "app won't install",
543            "installer stuck",
544            "install stuck",
545            "setup stuck",
546            "store not working",
547            "winget"
548        ],
549        &[("installer_health", "Installer Health")]
550    );
551    add_if!(
552        &[
553            "no sound",
554            "audio not",
555            "sound not",
556            "speaker not",
557            "microphone not",
558            "mic not",
559            "audio stopped",
560            "crackling",
561            "no audio"
562        ],
563        &[("audio", "Audio")]
564    );
565    add_if!(
566        &[
567            "bluetooth not",
568            "bluetooth won't",
569            "headset won't connect",
570            "headphones won't",
571            "can't pair",
572            "won't pair",
573            "bluetooth disconnect",
574            "bluetooth keep"
575        ],
576        &[
577            ("bluetooth", "Bluetooth"),
578            ("device_health", "Device Health")
579        ]
580    );
581    add_if!(
582        &[
583            "outlook not",
584            "outlook won't",
585            "outlook crash",
586            "outlook slow",
587            "email not",
588            "email won't",
589            "email crash",
590            "calendar not",
591            "pst",
592            "ost file"
593        ],
594        &[("outlook", "Outlook")]
595    );
596    add_if!(
597        &[
598            "teams not",
599            "teams won't",
600            "teams crash",
601            "teams slow",
602            "teams black screen",
603            "teams audio",
604            "teams video",
605            "microsoft teams"
606        ],
607        &[("teams", "Teams")]
608    );
609    add_if!(
610        &[
611            "chrome slow",
612            "chrome crash",
613            "edge slow",
614            "edge crash",
615            "firefox slow",
616            "firefox crash",
617            "browser slow",
618            "browser crash",
619            "browser not",
620            "browser keeps"
621        ],
622        &[("browser_health", "Browser Health")]
623    );
624
625    add_if!(
626        &[
627            "screen flicker",
628            "display flicker",
629            "screen blink",
630            "monitor flicker",
631            "display artifact",
632            "screen glitch",
633            "black screen",
634            "screen goes black"
635        ],
636        &[
637            ("display_config", "Display Config"),
638            ("device_health", "Device Health"),
639            ("drivers", "Drivers")
640        ]
641    );
642    add_if!(
643        &[
644            "high disk",
645            "disk 100",
646            "disk usage",
647            "disk is full",
648            "disk almost full",
649            "disk at 100",
650            "storage full",
651            "no space left"
652        ],
653        &[
654            ("storage", "Storage"),
655            ("processes", "Processes"),
656            ("disk_health", "Disk Health")
657        ]
658    );
659    add_if!(
660        &[
661            "overheat",
662            "overheating",
663            "too hot",
664            "running hot",
665            "laptop hot",
666            "pc getting hot",
667            "temperature high",
668            "cpu temperature",
669            "thermal throttl",
670            "fan loud",
671            "fan noise",
672            "fans running",
673            "fan running",
674            "fans spinning",
675            "fan spinning",
676            "loud fan",
677            "fan always on",
678            "fan constantly",
679            "fan at max",
680            "fan at 100"
681        ],
682        &[
683            ("thermal", "Thermal"),
684            ("cpu_power", "CPU Power"),
685            ("overclocker", "GPU / Overclocker Telemetry")
686        ]
687    );
688    add_if!(
689        &[
690            "out of memory",
691            "low memory",
692            "ram full",
693            "running out of ram",
694            "memory full",
695            "ram usage high",
696            "ram usage",
697            "memory usage high",
698            "physical memory",
699            "ram almost",
700            "not enough memory",
701            "memory leak"
702        ],
703        &[
704            ("resource_load", "Resource Load"),
705            ("pagefile", "Page File"),
706            ("processes", "Processes")
707        ]
708    );
709    add_if!(
710        &[
711            "gpu",
712            "graphics",
713            "game slow",
714            "games not",
715            "gaming performance",
716            "fps low",
717            "fps drop",
718            "game crash",
719            "video card",
720            "graphics card",
721            "stuttering"
722        ],
723        &[
724            ("overclocker", "GPU / Overclocker Telemetry"),
725            ("thermal", "Thermal"),
726            ("resource_load", "Resource Load"),
727            ("drivers", "Drivers")
728        ]
729    );
730    add_if!(
731        &[
732            "nvlddmkm",
733            "amdkmdag",
734            "tdr failure",
735            "video_tdr_failure",
736            "gpu driver crash",
737            "gpu driver stopped",
738            "gpu hang",
739            "display adapter error"
740        ],
741        &[
742            ("device_health", "Device Health"),
743            ("drivers", "Drivers"),
744            ("recent_crashes", "Recent Crashes")
745        ]
746    );
747    add_if!(
748        &[
749            "startup slow",
750            "boot slow",
751            "slow boot",
752            "takes long to start",
753            "slow to start",
754            "long boot"
755        ],
756        &[
757            ("startup_items", "Startup Items"),
758            ("resource_load", "Resource Load"),
759            ("services", "Services")
760        ]
761    );
762    add_if!(
763        &[
764            "no sound",
765            "audio not",
766            "sound not",
767            "speaker not",
768            "microphone not",
769            "mic not",
770            "audio stopped",
771            "crackling audio",
772            "distorted sound",
773            "no audio"
774        ],
775        &[("audio", "Audio")]
776    );
777    add_if!(
778        &[
779            "install app",
780            "install fail",
781            "can't install",
782            "installation fail",
783            "app won't install",
784            "setup fail",
785            "winget fail",
786            "store install"
787        ],
788        &[
789            ("installer_health", "Installer Health"),
790            ("pending_reboot", "Pending Reboot")
791        ]
792    );
793    add_if!(
794        &[
795            "usb",
796            "usb not recognized",
797            "usb not working",
798            "device not recognized",
799            "device manager error",
800            "device manager",
801            "unknown device",
802            "code 43",
803            "code 10",
804            "yellow bang",
805            "pnp error",
806            "driver missing"
807        ],
808        &[
809            ("device_health", "Device Health"),
810            ("drivers", "Drivers"),
811            ("usb_history", "USB History")
812        ]
813    );
814    add_if!(
815        &[
816            "no wifi",
817            "no wi-fi",
818            "can't find wifi",
819            "can't find wi-fi",
820            "wifi not showing",
821            "no wireless networks",
822            "no networks found",
823            "wifi adapter",
824            "wireless adapter"
825        ],
826        &[
827            ("wifi", "Wi-Fi"),
828            ("device_health", "Device Health"),
829            ("drivers", "Drivers")
830        ]
831    );
832    add_if!(
833        &[
834            "network share",
835            "shared folder",
836            "mapped drive",
837            "unc path",
838            "can't access share",
839            "network drive",
840            "smb share",
841            "\\\\server",
842            "file share"
843        ],
844        &[
845            ("share_access", "Share Access"),
846            ("connectivity", "Connectivity"),
847            ("shares", "Shares")
848        ]
849    );
850    add_if!(
851        &[
852            "microsoft store",
853            "windows store",
854            "appx",
855            "store not",
856            "store won't",
857            "store app",
858            "wsreset"
859        ],
860        &[
861            ("installer_health", "Installer Health"),
862            ("pending_reboot", "Pending Reboot")
863        ]
864    );
865    add_if!(
866        &[
867            "sleep",
868            "hibernate",
869            "won't wake",
870            "won't sleep",
871            "stuck after sleep",
872            "wake from sleep",
873            "keeps waking",
874            "fast startup",
875            "power issue",
876            "power problem"
877        ],
878        &[
879            ("log_check", "Event Log"),
880            ("startup_items", "Startup Items"),
881            ("services", "Services")
882        ]
883    );
884    add_if!(
885        &[
886            "keyboard",
887            "mouse not",
888            "touchpad",
889            "trackpad",
890            "input device",
891            "keyboard not",
892            "mouse frozen",
893            "keyboard frozen"
894        ],
895        &[
896            ("peripherals", "Peripherals"),
897            ("device_health", "Device Health")
898        ]
899    );
900    add_if!(
901        &[
902            "bandwidth",
903            "high network",
904            "network usage",
905            "using all",
906            "slow internet",
907            "data usage",
908            "upload slow",
909            "download slow",
910            "network slow"
911        ],
912        &[
913            ("network_stats", "Network Stats"),
914            ("connections", "Active Connections"),
915            ("processes", "Processes")
916        ]
917    );
918    add_if!(
919        &[
920            "wifi disconnects",
921            "wifi drops",
922            "wifi keeps dropping",
923            "wifi keeps disconnecting",
924            "internet keeps cutting",
925            "connection drops",
926            "wifi intermittent",
927            "wifi unstable"
928        ],
929        &[
930            ("wifi", "Wi-Fi"),
931            ("network_adapter", "Network Adapter"),
932            ("connectivity", "Connectivity")
933        ]
934    );
935    add_if!(
936        &[
937            "msmpeng",
938            "antimalware service",
939            "defender high cpu",
940            "wdnissvc",
941            "defender scan"
942        ],
943        &[
944            ("resource_load", "Resource Load"),
945            ("security", "Security"),
946            ("processes", "Processes")
947        ]
948    );
949    add_if!(
950        &[
951            "monitor not detected",
952            "second monitor",
953            "hdmi not working",
954            "hdmi not detected",
955            "displayport not",
956            "external monitor",
957            "external display",
958            "no signal on monitor",
959            "no signal on screen",
960            "monitor not showing",
961            "screen not detected",
962            "extend display",
963            "duplicate display",
964            "second screen"
965        ],
966        &[
967            ("display_config", "Display Config"),
968            ("device_health", "Device Health"),
969            ("drivers", "Drivers")
970        ]
971    );
972    add_if!(
973        &[
974            "explorer.exe crash",
975            "windows explorer crash",
976            "file explorer crash",
977            "file explorer",
978            "explorer crash",
979            "desktop icons disappeared",
980            "desktop disappeared",
981            "taskbar disappeared",
982            "taskbar not responding",
983            "taskbar missing",
984            "taskbar gone",
985            "start menu crashed",
986            "start menu not working",
987            "start menu broken",
988            "desktop froze"
989        ],
990        &[("processes", "Processes"), ("log_check", "Event Log")]
991    );
992    add_if!(
993        &[
994            "access denied",
995            "access is denied",
996            "permission denied",
997            "you don't have permission",
998            "cannot access this folder",
999            "file access denied",
1000            "folder access denied",
1001            "unauthorized access"
1002        ],
1003        &[("user_accounts", "User Accounts"), ("shares", "Shares")]
1004    );
1005
1006    // ── Network addressing ──────────────────────────────────────────────────
1007    add_if!(
1008        &[
1009            "ip address conflict",
1010            "no ip address",
1011            "can't get ip",
1012            "apipa",
1013            "169.254",
1014            "ipv4",
1015            "dhcp not",
1016            "dhcp fail",
1017            "dhcp server",
1018            "dhcp lease",
1019            "ip not assigned",
1020            "no dhcp"
1021        ],
1022        &[
1023            ("dhcp", "DHCP Lease"),
1024            ("ip_config", "IP Config"),
1025            ("connectivity", "Connectivity")
1026        ]
1027    );
1028    add_if!(
1029        &[
1030            "ipv6",
1031            "ipv6 not",
1032            "ipv6 fail",
1033            "no ipv6",
1034            "ipv6 issue",
1035            "ipv6 address",
1036            "slaac",
1037            "dhcpv6"
1038        ],
1039        &[("ipv6", "IPv6"), ("connectivity", "Connectivity")]
1040    );
1041    add_if!(
1042        &[
1043            "mtu",
1044            "packet fragmentation",
1045            "pmtu",
1046            "fragmented packet",
1047            "mtu mismatch"
1048        ],
1049        &[("mtu", "MTU"), ("connectivity", "Connectivity")]
1050    );
1051    // ── Security / compliance ───────────────────────────────────────────────
1052    add_if!(
1053        &[
1054            "certificate expired",
1055            "certificate is expired",
1056            "certificate error",
1057            "ssl error",
1058            "cert expired",
1059            "certificate not trusted",
1060            "ssl certificate",
1061            "tls error",
1062            "tls handshake",
1063            "tls fail",
1064            "certificate warning"
1065        ],
1066        &[("certificates", "Certificates")]
1067    );
1068    add_if!(
1069        &[
1070            "tpm not",
1071            "no tpm",
1072            "tpm missing",
1073            "tpm error",
1074            "tpm chip",
1075            "secure boot",
1076            "uefi secure",
1077            "bitlocker needs tpm"
1078        ],
1079        &[("tpm", "TPM / Secure Boot")]
1080    );
1081    add_if!(
1082        &[
1083            "smb1",
1084            "smb version 1",
1085            "ntlmv1",
1086            "ntlm level",
1087            "ntlm authentication level",
1088            "lmcompatibility",
1089            "lan manager"
1090        ],
1091        &[
1092            ("shares", "SMB Shares"),
1093            ("local_security_policy", "Local Security Policy")
1094        ]
1095    );
1096    // ── Storage / system files ──────────────────────────────────────────────
1097    add_if!(
1098        &[
1099            "pagefile",
1100            "page file",
1101            "hiberfil",
1102            "hiberfile",
1103            "virtual memory file",
1104            "virtual memory low",
1105            "memory commit",
1106            "commit charge",
1107            "swapfile.sys"
1108        ],
1109        &[("pagefile", "Page File"), ("storage", "Storage")]
1110    );
1111    add_if!(
1112        &[
1113            "windows search",
1114            "search index",
1115            "search indexing",
1116            "wsearch",
1117            "search eating",
1118            "cortana search",
1119            "cortana not",
1120            "search bar not"
1121        ],
1122        &[("search_index", "Search Index"), ("storage", "Storage")]
1123    );
1124    // ── System health ───────────────────────────────────────────────────────
1125    add_if!(
1126        &[
1127            "wmi not",
1128            "wmi is",
1129            "wmi error",
1130            "wmi corrupt",
1131            "wmi fail",
1132            "wmi broken",
1133            "winmgmt"
1134        ],
1135        &[("wmi_health", "WMI Health")]
1136    );
1137    add_if!(
1138        &[
1139            "event log full",
1140            "event log error",
1141            "event viewer",
1142            "event log cleared",
1143            "audit log"
1144        ],
1145        &[("log_check", "Event Log"), ("services", "Services")]
1146    );
1147    add_if!(
1148        &[
1149            "ctrl+alt+del",
1150            "login screen stuck",
1151            "stuck at login",
1152            "stuck on login",
1153            "login loop",
1154            "sign-in loop",
1155            "sign in loop",
1156            "reboot loop on login",
1157            "stuck on lock screen",
1158            "winlogon",
1159            "sign in screen stuck",
1160            "can't get past login",
1161            "welcome screen stuck"
1162        ],
1163        &[
1164            ("sign_in", "Sign-In / Windows Hello"),
1165            ("services", "Services")
1166        ]
1167    );
1168    add_if!(
1169        &[
1170            "reset windows",
1171            "can't reset",
1172            "windows reset",
1173            "reinstall windows",
1174            "factory reset windows",
1175            "repair windows install",
1176            "windows recovery"
1177        ],
1178        &[
1179            ("installer_health", "Installer Health"),
1180            ("pending_reboot", "Pending Reboot"),
1181            ("integrity", "Integrity")
1182        ]
1183    );
1184    add_if!(
1185        &[
1186            "scheduled task",
1187            "task scheduler",
1188            "task not running",
1189            "scheduled job",
1190            "task failed"
1191        ],
1192        &[
1193            ("scheduled_tasks", "Scheduled Tasks"),
1194            ("log_check", "Event Log")
1195        ]
1196    );
1197    add_if!(
1198        &[
1199            "ssh connection",
1200            "ssh refused",
1201            "ssh timeout",
1202            "openssh",
1203            "ssh not working",
1204            "ssh key",
1205            "sshd",
1206            "known_hosts",
1207            "ssh host key",
1208            "ssh permissions"
1209        ],
1210        &[
1211            ("ssh", "SSH"),
1212            ("services", "Services"),
1213            ("firewall_rules", "Firewall Rules"),
1214            ("connectivity", "Connectivity")
1215        ]
1216    );
1217    add_if!(
1218        &[
1219            "git credential",
1220            "git push denied",
1221            "git auth",
1222            "github auth",
1223            "git permission",
1224            "git clone failed",
1225            "git identity",
1226            "git signing",
1227            "git commit sign",
1228            "commit signing",
1229            "fails sign",
1230            "gpg signing",
1231            "gpg key git",
1232            "git gpg"
1233        ],
1234        &[("git_config", "Git Config"), ("credentials", "Credentials")]
1235    );
1236    add_if!(
1237        &[
1238            "not found in path",
1239            "command not found",
1240            "python not found",
1241            "node not found",
1242            "node.js version",
1243            "node.js not",
1244            "cargo not",
1245            "pip not",
1246            "pip install",
1247            "conda environment",
1248            "conda env",
1249            "path issue",
1250            "missing from path",
1251            "env var not set",
1252            "toolchain not found"
1253        ],
1254        &[
1255            ("toolchains", "Toolchains"),
1256            ("env", "Environment Vars"),
1257            ("path", "PATH")
1258        ]
1259    );
1260    add_if!(
1261        &[
1262            "version conflict",
1263            "node version",
1264            "python version conflict",
1265            "dev conflict",
1266            "package manager conflict",
1267            "conda shadow",
1268            "nvm conflict",
1269            "pyenv conflict"
1270        ],
1271        &[
1272            ("dev_conflicts", "Dev Conflicts"),
1273            ("toolchains", "Toolchains")
1274        ]
1275    );
1276    add_if!(
1277        &[
1278            "group policy",
1279            "gpo ",
1280            "gpo:",
1281            "group policy error",
1282            "policy enforcement"
1283        ],
1284        &[("gpo", "Group Policy"), ("domain_health", "Domain Health")]
1285    );
1286    add_if!(
1287        &[
1288            "windows license",
1289            "not activated",
1290            "activate windows",
1291            "activation error",
1292            "product key",
1293            "license expired",
1294            "license invalid",
1295            "windows isn't activated",
1296            "not genuine",
1297            "windows is not genuine",
1298            "genuine check"
1299        ],
1300        &[("activation", "Windows Activation")]
1301    );
1302    add_if!(
1303        &[
1304            "bitlocker",
1305            "recovery key",
1306            "bitlocker locked",
1307            "drive encryption",
1308            "encrypted drive",
1309            "decryption failed"
1310        ],
1311        &[("bitlocker", "BitLocker"), ("tpm", "TPM / Secure Boot")]
1312    );
1313    add_if!(
1314        &[
1315            "can't join domain",
1316            "domain join",
1317            "domain controller",
1318            "domain not reachable",
1319            "kerberos",
1320            "ldap error",
1321            "dc unreachable",
1322            "gpo not applying",
1323            "group policy not",
1324            "group policy error",
1325            "policy not applied"
1326        ],
1327        &[
1328            ("domain_health", "Domain Health"),
1329            ("gpo", "Group Policy"),
1330            ("identity_auth", "Identity & Auth")
1331        ]
1332    );
1333    add_if!(
1334        &[
1335            "hyper-v",
1336            "hyperv",
1337            "vm won't start",
1338            "virtual machine won't",
1339            "vm not starting",
1340            "virtual machine error",
1341            "vm crash",
1342            "vm network"
1343        ],
1344        &[
1345            ("hyperv", "Hyper-V"),
1346            ("storage", "Storage"),
1347            ("disk_health", "Disk Health")
1348        ]
1349    );
1350    add_if!(
1351        &[
1352            "wsl",
1353            "wsl2",
1354            "linux subsystem",
1355            "wsl not",
1356            "wsl won't",
1357            "linux on windows",
1358            "bash on windows"
1359        ],
1360        &[
1361            ("wsl", "WSL"),
1362            ("wsl_filesystems", "WSL Filesystems"),
1363            ("connectivity", "Connectivity"),
1364            ("dns_servers", "DNS Servers")
1365        ]
1366    );
1367    add_if!(
1368        &[
1369            "docker",
1370            "container",
1371            "docker compose",
1372            "docker daemon",
1373            "docker not",
1374            "docker won't"
1375        ],
1376        &[
1377            ("docker", "Docker"),
1378            ("docker_filesystems", "Docker Filesystems"),
1379            ("connectivity", "Connectivity")
1380        ]
1381    );
1382    add_if!(
1383        &[
1384            "restarts randomly",
1385            "keeps restarting",
1386            "random restart",
1387            "random reboot",
1388            "reboot randomly",
1389            "reboots itself",
1390            "spontaneous restart",
1391            "unexpected reboot",
1392            "auto restart"
1393        ],
1394        &[
1395            ("recent_crashes", "Crash History"),
1396            ("log_check", "Event Log"),
1397            ("pending_reboot", "Pending Reboot")
1398        ]
1399    );
1400    add_if!(
1401        &[
1402            "disk filling up",
1403            "drive filling",
1404            "storage filling",
1405            "ssd filling",
1406            "getting full",
1407            "filling up fast",
1408            "recycle bin",
1409            "temp files growing",
1410            "disk growing"
1411        ],
1412        &[
1413            ("storage", "Storage"),
1414            ("disk_health", "Disk Health"),
1415            ("shadow_copies", "Shadow Copies")
1416        ]
1417    );
1418
1419    add_if!(
1420        &[
1421            "how much ram",
1422            "check my ram",
1423            "check ram",
1424            "my ram size",
1425            "show ram",
1426            "what cpu",
1427            "cpu model",
1428            "what processor",
1429            "ram size",
1430            "motherboard model",
1431            "system specs",
1432            "hardware specs",
1433            "bios version",
1434            "what gpu",
1435            "gpu model",
1436            "hardware info",
1437            "check specs"
1438        ],
1439        &[("hardware", "Hardware")]
1440    );
1441    add_if!(
1442        &[
1443            "windows version",
1444            "what version of windows",
1445            "windows 10 or 11",
1446            "os version",
1447            "build number",
1448            "windows build",
1449            "edition of windows",
1450            "which windows"
1451        ],
1452        &[("os_config", "OS Config")]
1453    );
1454    add_if!(
1455        &[
1456            "someone accessed",
1457            "unauthorized access",
1458            "who logged in",
1459            "remote access log",
1460            "logon sessions",
1461            "active sessions",
1462            "who is connected"
1463        ],
1464        &[
1465            ("sessions", "Logon Sessions"),
1466            ("security", "Security Posture"),
1467            ("log_check", "Event Log")
1468        ]
1469    );
1470    add_if!(
1471        &[
1472            "ethernet not",
1473            "ethernet port",
1474            "wired connection not",
1475            "wired network not",
1476            "nic not working",
1477            "network adapter not",
1478            "network card",
1479            "ethernet cable"
1480        ],
1481        &[
1482            ("network_adapter", "Network Adapter"),
1483            ("connectivity", "Connectivity"),
1484            ("device_health", "Device Health")
1485        ]
1486    );
1487    add_if!(
1488        &[
1489            "task manager",
1490            "process explorer",
1491            "what processes",
1492            "most resources",
1493            "using the most",
1494            "top processes",
1495            "resource hog"
1496        ],
1497        &[
1498            ("processes", "Processes"),
1499            ("resource_load", "Resource Load")
1500        ]
1501    );
1502    add_if!(
1503        &[
1504            "battery",
1505            "not charging",
1506            "battery drain",
1507            "battery low",
1508            "battery dead",
1509            "won't charge",
1510            "wont charge",
1511            "plugged in not charging",
1512            "power adapter",
1513            "ac adapter",
1514            "battery percentage"
1515        ],
1516        &[
1517            ("battery", "Battery"),
1518            ("thermal", "Thermal"),
1519            ("cpu_power", "CPU Power")
1520        ]
1521    );
1522    add_if!(
1523        &[
1524            "disk benchmark",
1525            "storage speed",
1526            "read write speed",
1527            "sequential read",
1528            "sequential write",
1529            "io throughput",
1530            "disk throughput",
1531            "disk performance",
1532            "drive speed test"
1533        ],
1534        &[("disk_benchmark", "Disk Benchmark")]
1535    );
1536    add_if!(
1537        &[
1538            "uac prompt",
1539            "uac not",
1540            "uac disabled",
1541            "user account control",
1542            "admin rights",
1543            "needs admin",
1544            "needs elevation",
1545            "run as administrator",
1546            "administrator permission"
1547        ],
1548        &[
1549            ("local_security_policy", "Local Security Policy"),
1550            ("user_accounts", "User Accounts")
1551        ]
1552    );
1553
1554    if topics.is_empty() {
1555        topics.push(("health_report", "System Health"));
1556        topics.push(("log_check", "Event Log"));
1557    }
1558    topics
1559}
1560
1561pub fn fix_plan_topics(issue: &str) -> Vec<(&'static str, &'static str)> {
1562    topics_for_issue(issue)
1563}
1564
1565/// A safe auto-executable fix with optional post-fix verification.
1566/// `verify_topic`: inspect_host topic to re-run before (pre-check) and after the fix.
1567/// `verify_gone`: pattern ABSENT in healthy output; present means the problem exists.
1568/// `include_in_sweep`: eligible for `--fix-all` maintenance sweep.
1569pub struct AutoFix {
1570    pub label: &'static str,
1571    pub cmd: &'static str,
1572    pub verify_topic: Option<&'static str>,
1573    pub verify_gone: Option<&'static str>,
1574    pub include_in_sweep: bool,
1575}
1576
1577struct AutoCmdAc {
1578    ac: aho_corasick::AhoCorasick,
1579    entries: Vec<AutoFix>,
1580}
1581
1582static AUTO_CMD_AC: std::sync::OnceLock<AutoCmdAc> = std::sync::OnceLock::new();
1583
1584fn auto_cmd_ac() -> &'static AutoCmdAc {
1585    AUTO_CMD_AC.get_or_init(|| {
1586        // (trigger, label, cmd, verify_topic, verify_gone, include_in_sweep)
1587        const SAFE: &[(&str, &str, &str, Option<&str>, Option<&str>, bool)] = &[
1588            (
1589                "dns: failed",
1590                "Flush DNS cache",
1591                "ipconfig /flushdns",
1592                Some("connectivity"),
1593                Some("dns: failed"),
1594                true,
1595            ),
1596            (
1597                "dns resolution: failed",
1598                "Flush DNS cache",
1599                "ipconfig /flushdns",
1600                Some("connectivity"),
1601                Some("dns: failed"),
1602                false, // duplicate label — sweep deduplicates by label
1603            ),
1604            (
1605                "wsearch",
1606                "Restart Windows Search",
1607                "powershell -Command \"Restart-Service WSearch -ErrorAction SilentlyContinue\"",
1608                Some("search_index"),
1609                Some("stopped"),
1610                true,
1611            ),
1612            (
1613                "windows search",
1614                "Restart Windows Search",
1615                "powershell -Command \"Restart-Service WSearch -ErrorAction SilentlyContinue\"",
1616                Some("search_index"),
1617                Some("stopped"),
1618                false, // duplicate label
1619            ),
1620            (
1621                "spooler",
1622                "Restart Print Spooler",
1623                "powershell -Command \"Restart-Service Spooler -Force\"",
1624                Some("printers"),
1625                Some("offline"),
1626                true,
1627            ),
1628            (
1629                "print spooler",
1630                "Restart Print Spooler",
1631                "powershell -Command \"Restart-Service Spooler -Force\"",
1632                Some("printers"),
1633                Some("offline"),
1634                false, // duplicate label
1635            ),
1636            (
1637                "ntp source unreachable",
1638                "Resync system clock",
1639                "w32tm /resync /force",
1640                Some("ntp"),
1641                Some("failed"),
1642                true,
1643            ),
1644            (
1645                "time sync failed",
1646                "Resync system clock",
1647                "w32tm /resync /force",
1648                Some("ntp"),
1649                Some("failed"),
1650                false, // duplicate label
1651            ),
1652            (
1653                "bits",
1654                "Restart BITS service",
1655                "powershell -Command \"Restart-Service BITS -Force\"",
1656                None,
1657                None,
1658                false, // no verify — skip sweep
1659            ),
1660            (
1661                "wuauserv",
1662                "Restart Windows Update service",
1663                "powershell -Command \"Restart-Service wuauserv -Force\"",
1664                None,
1665                None,
1666                false,
1667            ),
1668            (
1669                "windows update service",
1670                "Restart Windows Update service",
1671                "powershell -Command \"Restart-Service wuauserv -Force\"",
1672                None,
1673                None,
1674                false, // duplicate label
1675            ),
1676            (
1677                "audiosrv",
1678                "Restart Audio service",
1679                "powershell -Command \"Restart-Service Audiosrv -Force\"",
1680                Some("audio"),
1681                Some("not running"),
1682                true,
1683            ),
1684            (
1685                "windows audio",
1686                "Restart Audio service",
1687                "powershell -Command \"Restart-Service Audiosrv -Force\"",
1688                Some("audio"),
1689                Some("not running"),
1690                false, // duplicate label
1691            ),
1692            (
1693                "low disk",
1694                "Empty Recycle Bin",
1695                "powershell -Command \"Clear-RecycleBin -Force -ErrorAction SilentlyContinue\"",
1696                None,
1697                None,
1698                true, // always safe — include in sweep
1699            ),
1700            (
1701                "free up space",
1702                "Empty Recycle Bin",
1703                "powershell -Command \"Clear-RecycleBin -Force -ErrorAction SilentlyContinue\"",
1704                None,
1705                None,
1706                false, // duplicate label
1707            ),
1708            // Teams cache — classic and new Teams
1709            (
1710                "teams cache",
1711                "Clear Teams cache",
1712                "powershell -Command \"Get-Process ms-teams,Teams -ErrorAction SilentlyContinue | Stop-Process -Force; Remove-Item \\\"$env:APPDATA\\\\Microsoft\\\\Teams\\\\Cache\\\\*\\\" -Recurse -Force -ErrorAction SilentlyContinue; Remove-Item \\\"$env:LOCALAPPDATA\\\\Packages\\\\MSTeams_8wekyb3d8bbwe\\\\LocalCache\\\\Microsoft\\\\MSTeams\\\\EBWebView\\\\*\\\" -Recurse -Force -ErrorAction SilentlyContinue\"",
1713                Some("teams"),
1714                Some("cache:"),
1715                true,
1716            ),
1717            (
1718                "msteams",
1719                "Clear Teams cache",
1720                "powershell -Command \"Get-Process ms-teams,Teams -ErrorAction SilentlyContinue | Stop-Process -Force; Remove-Item \\\"$env:APPDATA\\\\Microsoft\\\\Teams\\\\Cache\\\\*\\\" -Recurse -Force -ErrorAction SilentlyContinue; Remove-Item \\\"$env:LOCALAPPDATA\\\\Packages\\\\MSTeams_8wekyb3d8bbwe\\\\LocalCache\\\\Microsoft\\\\MSTeams\\\\EBWebView\\\\*\\\" -Recurse -Force -ErrorAction SilentlyContinue\"",
1721                Some("teams"),
1722                Some("cache:"),
1723                false, // duplicate label
1724            ),
1725            // Bluetooth
1726            (
1727                "bluetooth service",
1728                "Restart Bluetooth service",
1729                "powershell -Command \"Restart-Service bthserv -Force -ErrorAction SilentlyContinue\"",
1730                Some("bluetooth"),
1731                Some("not running"),
1732                true,
1733            ),
1734            (
1735                "bthserv",
1736                "Restart Bluetooth service",
1737                "powershell -Command \"Restart-Service bthserv -Force -ErrorAction SilentlyContinue\"",
1738                Some("bluetooth"),
1739                Some("not running"),
1740                false, // duplicate label
1741            ),
1742            // DHCP — renewing drops network briefly; skip sweep
1743            (
1744                "dhcp lease expired",
1745                "Renew DHCP lease",
1746                "ipconfig /release && ipconfig /renew",
1747                Some("dhcp"),
1748                Some("expired"),
1749                false,
1750            ),
1751            (
1752                "lease expires",
1753                "Renew DHCP lease",
1754                "ipconfig /release && ipconfig /renew",
1755                Some("dhcp"),
1756                Some("expired"),
1757                false, // duplicate label
1758            ),
1759            // DNS client service — covered by DNS flush in sweep
1760            (
1761                "dnscache",
1762                "Restart DNS Client service",
1763                "powershell -Command \"Restart-Service Dnscache -Force -ErrorAction SilentlyContinue\"",
1764                Some("connectivity"),
1765                Some("dns: failed"),
1766                false,
1767            ),
1768            // OneDrive
1769            (
1770                "onedrive not running",
1771                "Start OneDrive",
1772                "powershell -Command \"Start-Process \\\"$env:LOCALAPPDATA\\\\Microsoft\\\\OneDrive\\\\OneDrive.exe\\\" -ErrorAction SilentlyContinue\"",
1773                Some("onedrive"),
1774                Some("not running"),
1775                true,
1776            ),
1777            // WMI — disruptive; only on explicit --fix, not sweep
1778            (
1779                "wmi repository",
1780                "Restart WMI service",
1781                "powershell -Command \"Restart-Service winmgmt -Force\"",
1782                Some("wmi_health"),
1783                Some("corrupt"),
1784                false,
1785            ),
1786            (
1787                "winmgmt",
1788                "Restart WMI service",
1789                "powershell -Command \"Restart-Service winmgmt -Force\"",
1790                Some("wmi_health"),
1791                Some("corrupt"),
1792                false, // duplicate label
1793            ),
1794            // Network Location Awareness
1795            (
1796                "unidentified network",
1797                "Restart Network Location Awareness",
1798                "powershell -Command \"Restart-Service NlaSvc -Force -ErrorAction SilentlyContinue\"",
1799                Some("network_profile"),
1800                Some("unidentified"),
1801                true,
1802            ),
1803            // Windows Temp folder — always safe, include in sweep
1804            (
1805                "temp folder",
1806                "Clear Windows Temp folder",
1807                "powershell -Command \"Remove-Item \\\"$env:TEMP\\\\*\\\" -Recurse -Force -ErrorAction SilentlyContinue\"",
1808                None,
1809                None,
1810                true, // always safe — runs every sweep
1811            ),
1812            (
1813                "temporary files",
1814                "Clear Windows Temp folder",
1815                "powershell -Command \"Remove-Item \\\"$env:TEMP\\\\*\\\" -Recurse -Force -ErrorAction SilentlyContinue\"",
1816                None,
1817                None,
1818                false, // duplicate label
1819            ),
1820            // Windows Firewall
1821            (
1822                "firewall: off",
1823                "Restart Windows Firewall",
1824                "powershell -Command \"Restart-Service MpsSvc -Force -ErrorAction SilentlyContinue\"",
1825                Some("security"),
1826                Some("firewall: off"),
1827                true,
1828            ),
1829            (
1830                "firewall profile: disabled",
1831                "Restart Windows Firewall",
1832                "powershell -Command \"Restart-Service MpsSvc -Force -ErrorAction SilentlyContinue\"",
1833                Some("security"),
1834                Some("firewall: off"),
1835                false, // duplicate label
1836            ),
1837            // TCP/IP stack reset — requires reboot; explicit --fix only
1838            (
1839                "winsock",
1840                "Reset TCP/IP stack",
1841                "netsh int ip reset && netsh winsock reset",
1842                Some("connectivity"),
1843                Some("unreachable"),
1844                false,
1845            ),
1846            // Remote Desktop service — explicit --fix only (disruptive if active sessions)
1847            (
1848                "termservice",
1849                "Restart Remote Desktop Services",
1850                "powershell -Command \"Restart-Service TermService -Force -ErrorAction SilentlyContinue\"",
1851                Some("rdp"),
1852                Some("stopped"),
1853                false,
1854            ),
1855            // WLAN AutoConfig — explicit --fix only (could drop Wi-Fi briefly)
1856            (
1857                "wlansvc",
1858                "Restart WLAN AutoConfig service",
1859                "powershell -Command \"Restart-Service Wlansvc -Force -ErrorAction SilentlyContinue\"",
1860                Some("wifi"),
1861                Some("stopped"),
1862                false,
1863            ),
1864            // Cryptographic Services
1865            (
1866                "cryptsvc",
1867                "Restart Cryptographic Services",
1868                "powershell -Command \"Restart-Service CryptSvc -Force -ErrorAction SilentlyContinue\"",
1869                Some("identity_auth"),
1870                Some("cryptsvc"),
1871                false,
1872            ),
1873            // Microsoft Store — wsreset is safe, clears cache only
1874            (
1875                "microsoft.windowsstore | status: missing",
1876                "Reset Microsoft Store cache",
1877                "%SystemRoot%\\System32\\wsreset.exe",
1878                None,
1879                None,
1880                false, // interactive window opens; not suitable for sweep
1881            ),
1882            // Browser cache — Edge only (Chrome requires profile path resolution; explicit --fix only)
1883            (
1884                "browser slow",
1885                "Clear Microsoft Edge cache",
1886                "powershell -Command \"Remove-Item \\\"$env:LOCALAPPDATA\\\\Microsoft\\\\Edge\\\\User Data\\\\Default\\\\Cache\\\\*\\\" -Recurse -Force -ErrorAction SilentlyContinue\"",
1887                None,
1888                None,
1889                false, // user data — explicit --fix only, never sweep
1890            ),
1891            (
1892                "browser crashing",
1893                "Clear Microsoft Edge cache",
1894                "powershell -Command \"Remove-Item \\\"$env:LOCALAPPDATA\\\\Microsoft\\\\Edge\\\\User Data\\\\Default\\\\Cache\\\\*\\\" -Recurse -Force -ErrorAction SilentlyContinue\"",
1895                None,
1896                None,
1897                false, // duplicate label
1898            ),
1899            // VPN — RasMan manages all VPN tunnels; safe to restart
1900            (
1901                "rasman",
1902                "Restart Remote Access Connection Manager (VPN)",
1903                "powershell -Command \"Restart-Service RasMan -Force -ErrorAction SilentlyContinue\"",
1904                Some("vpn"),
1905                Some("not running"),
1906                false, // could drop active VPN tunnel; explicit --fix only
1907            ),
1908            (
1909                "vpn adapter detected",
1910                "Restart Remote Access Connection Manager (VPN)",
1911                "powershell -Command \"Restart-Service RasMan -Force -ErrorAction SilentlyContinue\"",
1912                Some("vpn"),
1913                Some("not running"),
1914                false, // duplicate label
1915            ),
1916            // Windows Update cache reset — stops services, deletes SoftwareDistribution; explicit only
1917            (
1918                "update stuck downloading",
1919                "Reset Windows Update components",
1920                "powershell -Command \"net stop wuauserv; net stop bits; Remove-Item \\\"C:\\\\Windows\\\\SoftwareDistribution\\\\*\\\" -Recurse -Force -ErrorAction SilentlyContinue; net start wuauserv; net start bits\"",
1921                None,
1922                None,
1923                false, // disruptive — never include in sweep
1924            ),
1925            (
1926                "update error 0x",
1927                "Reset Windows Update components",
1928                "powershell -Command \"net stop wuauserv; net stop bits; Remove-Item \\\"C:\\\\Windows\\\\SoftwareDistribution\\\\*\\\" -Recurse -Force -ErrorAction SilentlyContinue; net start wuauserv; net start bits\"",
1929                None,
1930                None,
1931                false, // duplicate label
1932            ),
1933            // Remote Desktop — security-sensitive; only on explicit --fix
1934            (
1935                "remote desktop disabled",
1936                "Enable Remote Desktop",
1937                "powershell -Command \"Set-ItemProperty -Path 'HKLM:\\System\\CurrentControlSet\\Control\\Terminal Server' -Name fDenyTSConnections -Value 0; Enable-NetFirewallRule -DisplayGroup 'Remote Desktop' -ErrorAction SilentlyContinue\"",
1938                Some("rdp"),
1939                Some("disabled"),
1940                false,
1941            ),
1942        ];
1943        let mut patterns: Vec<&str> = Vec::with_capacity(SAFE.len());
1944        let mut entries: Vec<AutoFix> = Vec::with_capacity(SAFE.len());
1945        for &(trigger, label, cmd, verify_topic, verify_gone, include_in_sweep) in SAFE {
1946            patterns.push(trigger);
1947            entries.push(AutoFix { label, cmd, verify_topic, verify_gone, include_in_sweep });
1948        }
1949        AutoCmdAc {
1950            ac: aho_corasick::AhoCorasick::new(&patterns).expect("valid patterns"),
1951            entries,
1952        }
1953    })
1954}
1955
1956pub fn fix_plan_auto_commands(combined_output: &str) -> Vec<&'static AutoFix> {
1957    let lower = combined_output.to_ascii_lowercase();
1958    let state = auto_cmd_ac();
1959    let mut seen_labels = std::collections::HashSet::new();
1960    let mut result: Vec<&'static AutoFix> = Vec::new();
1961    for mat in state.ac.find_iter(&lower) {
1962        let fix = &state.entries[mat.pattern().as_usize()];
1963        if seen_labels.insert(fix.label) {
1964            result.push(fix);
1965        }
1966    }
1967    result
1968}
1969
1970/// Returns the ordered list of fixes eligible for `--fix-all` maintenance sweep.
1971/// One entry per label, sweep-flagged only.
1972pub fn sweep_auto_fixes() -> Vec<&'static AutoFix> {
1973    let state = auto_cmd_ac();
1974    let mut seen = std::collections::HashSet::new();
1975    state
1976        .entries
1977        .iter()
1978        .filter(|f| f.include_in_sweep && seen.insert(f.label))
1979        .collect()
1980}
1981
1982/// Map a recipe title to the most natural `hematite --fix "<issue>"` argument.
1983/// Returns `None` for recipes that don't have a clean fix-command equivalent.
1984pub fn recipe_title_to_fix_arg(title: &str) -> Option<&'static str> {
1985    match title {
1986        t if t.contains("disk space") || t.contains("Low disk") => Some("disk full"),
1987        t if t.contains("Drive health") || t.contains("failure") => Some("disk health warning"),
1988        t if t.contains("Restart required") => Some("restart required"),
1989        t if t.contains("event log errors") => Some("Windows errors in event log"),
1990        t if t.contains("service not running") => Some("critical service stopped"),
1991        t if t.contains("No internet") => Some("can't connect to internet"),
1992        t if t.contains("latency") => Some("high network latency"),
1993        t if t.contains("memory usage") => Some("high RAM usage"),
1994        t if t.contains("running hot") || t.contains("CPU") => Some("CPU running hot"),
1995        t if t.contains("security protection") => Some("Windows Defender disabled"),
1996        t if t.contains("Threat detected") => Some("virus or malware detected"),
1997        t if t.contains("updates pending") => Some("Windows updates pending"),
1998        t if t.contains("Hardware device error") => Some("hardware device error"),
1999        t if t.contains("No backup") => Some("no backup configured"),
2000        t if t.contains("SMB1") => Some("SMB1 security risk"),
2001        t if t.contains("encryption not enabled") => Some("BitLocker not enabled"),
2002        t if t.contains("DNS resolution") => Some("DNS not resolving"),
2003        t if t.contains("Application crashing") => Some("app crashing repeatedly"),
2004        t if t.contains("Wi-Fi signal weak") => Some("weak Wi-Fi signal"),
2005        t if t.contains("clock not synchronizing") => Some("clock out of sync"),
2006        t if t.contains("system file corruption") => Some("Windows system files corrupt"),
2007        t if t.contains("Service failed") => Some("service stopped unexpectedly"),
2008        t if t.contains("Remote Desktop") => Some("RDP disabled or blocked"),
2009        t if t.contains("Windows Update service") => Some("Windows Update broken"),
2010        t if t.contains("Teams cache") => Some("Teams not working"),
2011        t if t.contains("authentication broker") => Some("Microsoft 365 sign-in broken"),
2012        t if t.contains("WMI repository") => Some("WMI errors"),
2013        t if t.contains("Windows not activated") => Some("Windows not activated"),
2014        t if t.contains("Windows Search") => Some("Windows search not finding files"),
2015        t if t.contains("OneDrive not syncing") => Some("OneDrive not syncing"),
2016        t if t.contains("Printer offline") => Some("printer offline or stuck"),
2017        t if t.contains("Outlook mail profile") => Some("Outlook not opening"),
2018        t if t.contains("PrintNightmare") => Some("PrintNightmare not mitigated"),
2019        t if t.contains("Temp folder") => Some("disk full"),
2020        t if t.contains("Windows Firewall") => Some("Windows Firewall stopped"),
2021        t if t.contains("TCP/IP") && t.contains("stack") => {
2022            Some("network not working after update")
2023        }
2024        t if t.contains("Remote Desktop Services") => Some("RDP not responding"),
2025        t if t.contains("WLAN AutoConfig") => Some("WiFi service stopped"),
2026        t if t.contains("Cryptographic Services") => Some("certificates not loading"),
2027        t if t.contains("No audio") => Some("no sound"),
2028        t if t.contains("Bluetooth not working") => Some("Bluetooth not connecting"),
2029        t if t.contains("App installation failing") => Some("app install not working"),
2030        t if t.contains("Blue screen") || t.contains("BSOD") => Some("BSOD or blue screen"),
2031        t if t.contains("Camera") && t.contains("webcam") => Some("camera not working"),
2032        t if t.contains("VPN not connecting") => Some("VPN not connecting"),
2033        t if t.contains("Screen flickering") => Some("screen flickering"),
2034        t if t.contains("Microphone not working") => Some("microphone not working"),
2035        t if t.contains("Login") && t.contains("PIN") => Some("PIN not working"),
2036        t if t.contains("Disk at 100%") => Some("disk at 100%"),
2037        t if t.contains("USB device not recognized") => Some("USB device not working"),
2038        t if t.contains("No Wi-Fi networks visible") => Some("no Wi-Fi networks showing"),
2039        t if t.contains("Network share") && t.contains("accessible") => {
2040            Some("network share not accessible")
2041        }
2042        t if t.contains("Microsoft Store") && t.contains("AppX") => {
2043            Some("Microsoft Store not working")
2044        }
2045        t if t.contains("sleep") && t.contains("hibernate") => Some("PC won't sleep or wake"),
2046        t if t.contains("Keyboard") && t.contains("mouse") => Some("keyboard not working"),
2047        t if t.contains("High network usage") => Some("high network usage"),
2048        t if t.contains("crackling") || t.contains("distortion") || t.contains("stuttering") => {
2049            Some("audio crackling")
2050        }
2051        t if t.contains("Browser") && (t.contains("slow") || t.contains("crashing")) => {
2052            Some("browser slow or crashing")
2053        }
2054        t if t.contains("Antimalware Service Executable") || t.contains("MsMpEng") => {
2055            Some("Defender high CPU")
2056        }
2057        t if t.contains("External monitor") || t.contains("no signal") => {
2058            Some("external monitor not detected")
2059        }
2060        t if t.contains("Explorer") && t.contains("crashed") => Some("desktop or taskbar crashed"),
2061        t if t.contains("Visual C++") => Some("Visual C++ runtime missing"),
2062        t if t.contains("Certificate expiring") => Some("certificate expiring soon"),
2063        t if t.contains("Page file not configured") => Some("page file not configured"),
2064        t if t.contains("startup is slow") || t.contains("long time to boot") => {
2065            Some("slow startup")
2066        }
2067        t if t.contains("Update stuck") => Some("Windows Update stuck"),
2068        t if t.contains("GPU") && t.contains("driver crash") => Some("GPU driver crash"),
2069        t if t.contains("Access denied") || t.contains("permission error") => {
2070            Some("access denied to file or folder")
2071        }
2072        t if t.contains("Wi-Fi keeps disconnecting") || t.contains("dropping intermittently") => {
2073            Some("Wi-Fi keeps dropping")
2074        }
2075        t if t.contains("Application crash history") => Some("app keeps crashing"),
2076        t if t.contains("Outlook add-in") || (t.contains("Outlook") && t.contains("OST")) => {
2077            Some("Outlook crashing or slow")
2078        }
2079        _ => None,
2080    }
2081}
2082
2083/// Given the combined plain-text content of a triage or diagnose report, return
2084/// a deduplicated list of `hematite --fix "<issue>"` suggestions for the IT tech.
2085/// Only ACTION and INVESTIGATE severity recipes are surfaced.
2086pub fn suggest_fix_commands(content: &str) -> Vec<String> {
2087    let recipes = crate::agent::fix_recipes::match_recipes(content);
2088    let mut seen = std::collections::HashSet::new();
2089    let mut suggestions: Vec<String> = Vec::new();
2090    for recipe in recipes {
2091        if recipe.severity == "MONITOR" {
2092            continue;
2093        }
2094        if let Some(arg) = recipe_title_to_fix_arg(recipe.title) {
2095            if seen.insert(arg) {
2096                suggestions.push(format!("  hematite --fix \"{}\"", arg));
2097            }
2098        }
2099    }
2100    suggestions
2101}
2102
2103/// Re-score health from a saved report's plain-text content for terminal display.
2104/// Wraps the whole content as a single pseudo-section and runs the recipe matcher.
2105pub fn score_health_from_content(content: &str) -> crate::agent::fix_recipes::HealthScore {
2106    crate::agent::fix_recipes::score_health(&[("report", content)])
2107}
2108
2109pub fn report_has_issues_in_content(content: &str) -> bool {
2110    for line in content.lines() {
2111        if line.contains("Health Score:") {
2112            if let Some(pos) = line.find("Score:") {
2113                let after = line[pos + 6..]
2114                    .trim_start()
2115                    .trim_start_matches('*')
2116                    .trim_start();
2117                return !after.starts_with('A');
2118            }
2119        }
2120    }
2121    false
2122}
2123
2124pub fn fix_issue_categories() -> &'static [(&'static str, &'static str)] {
2125    &[
2126        (
2127            "Performance",
2128            "slow, lag, freeze, hang, high cpu, high ram, unresponsive",
2129        ),
2130        (
2131            "Network",
2132            "internet, wifi, offline, no connection, can't browse",
2133        ),
2134        ("DNS", "dns, name resolution, can't resolve"),
2135        ("VPN", "vpn, tunnel, remote access"),
2136        (
2137            "Disk Space",
2138            "disk full, out of space, low disk, drive full",
2139        ),
2140        (
2141            "Disk Health",
2142            "disk fail, smart error, bad sector, drive health",
2143        ),
2144        (
2145            "Slow Boot",
2146            "slow boot, startup slow, takes forever to boot",
2147        ),
2148        (
2149            "Crash / BSOD",
2150            "crash, bsod, blue screen, stop error, kernel panic",
2151        ),
2152        (
2153            "App Crashes",
2154            "app crash, not responding, application error",
2155        ),
2156        (
2157            "Windows Update",
2158            "update, windows update, patch, stuck on update",
2159        ),
2160        (
2161            "Virus / Malware",
2162            "virus, malware, hacked, threat, infected, ransomware",
2163        ),
2164        ("Firewall", "firewall, blocked port, blocked connection"),
2165        ("Printer", "printer, printing, print queue, can't print"),
2166        ("Audio", "sound, audio, no sound, speaker, mic, microphone"),
2167        ("Bluetooth", "bluetooth, headphones, wireless headset"),
2168        ("Camera", "camera, webcam, video call"),
2169        ("Teams", "teams, microsoft teams"),
2170        (
2171            "Outlook / Email",
2172            "outlook, email not working, calendar not",
2173        ),
2174        ("Browser", "browser, chrome, edge, firefox, slow browser"),
2175        (
2176            "Sign-In / PIN",
2177            "sign in, can't log in, pin not working, fingerprint, locked out",
2178        ),
2179        (
2180            "Remote Desktop",
2181            "rdp, remote desktop, can't connect remotely",
2182        ),
2183        (
2184            "Driver / Device",
2185            "device not recognized, driver not, usb not working, yellow bang",
2186        ),
2187        ("Clock / Time", "time wrong, clock wrong, time sync"),
2188        ("OneDrive", "onedrive, file sync, not syncing"),
2189        ("WMI", "wmi error, powershell wmi"),
2190        (
2191            "Display / Monitor",
2192            "monitor, display, screen resolution, second monitor, refresh rate, scaling",
2193        ),
2194        (
2195            "Keyboard / Mouse",
2196            "keyboard not working, mouse not working, touchpad, trackpad",
2197        ),
2198        (
2199            "Sleep / Hibernate",
2200            "hibernate, won't sleep, won't wake, sleep issue, stuck after sleep",
2201        ),
2202        (
2203            "Microsoft Store / Apps",
2204            "microsoft store, store app, uwp, app won't install, winget",
2205        ),
2206        (
2207            "Network Share",
2208            "network share, mapped drive, unc path, smb share, can't access share",
2209        ),
2210        (
2211            "High Network Usage",
2212            "bandwidth, high network usage, slow internet, what's using network",
2213        ),
2214        (
2215            "USB Device",
2216            "usb not recognized, usb not working, device not recognized",
2217        ),
2218        (
2219            "GPU Driver Crash",
2220            "gpu driver crash, tdr failure, nvlddmkm, black screen after driver, display driver stopped",
2221        ),
2222        (
2223            "Windows Update Stuck",
2224            "update stuck, update error 0x, update won't install, cumulative update failed",
2225        ),
2226        (
2227            "Access Denied",
2228            "access denied, can't open file, permission denied, you don't have permission",
2229        ),
2230        (
2231            "Wi-Fi Dropping",
2232            "wifi drops, wifi disconnects, internet cuts out, wifi intermittent, wifi unstable",
2233        ),
2234        (
2235            "Defender High CPU",
2236            "MsMpEng high CPU, antimalware service executable, Defender slow, Defender scan CPU",
2237        ),
2238        (
2239            "Monitor Not Detected",
2240            "second monitor not showing, HDMI not working, external monitor, no signal, DisplayPort",
2241        ),
2242        (
2243            "Explorer / Desktop Crashed",
2244            "desktop crashed, taskbar disappeared, start menu not working, File Explorer crash",
2245        ),
2246        (
2247            "Overheating / Fan",
2248            "pc overheating, fan running loud, fans spinning, thermal throttling, cpu too hot",
2249        ),
2250        (
2251            "RAM / Memory",
2252            "RAM almost full, out of memory, low memory, memory leak, running out of ram",
2253        ),
2254        (
2255            "Windows Activation",
2256            "not activated, windows license expired, activate windows, product key",
2257        ),
2258        (
2259            "BitLocker",
2260            "bitlocker asking for recovery key, drive encryption, bitlocker locked",
2261        ),
2262        (
2263            "Domain / Group Policy",
2264            "can't join domain, group policy not applying, domain controller unreachable",
2265        ),
2266        (
2267            "Hyper-V / VM",
2268            "hyper-v vm won't start, virtual machine error, vm network not working",
2269        ),
2270        (
2271            "WSL",
2272            "wsl not working, wsl2 error, linux subsystem broken",
2273        ),
2274        (
2275            "Docker",
2276            "docker container won't start, docker daemon not running, docker compose error",
2277        ),
2278        (
2279            "Random Restart",
2280            "computer restarts randomly, keeps restarting, random reboot",
2281        ),
2282        (
2283            "Disk Filling Up",
2284            "ssd getting full fast, disk filling up, recycle bin won't empty, hiberfil.sys too big",
2285        ),
2286        (
2287            "DHCP / IP Address",
2288            "no ip address, dhcp not working, ip address conflict, apipa, 169.254",
2289        ),
2290        (
2291            "Certificate / SSL",
2292            "certificate expired, ssl error, cert not trusted, tls error",
2293        ),
2294        (
2295            "TPM / Secure Boot",
2296            "tpm not detected, secure boot disabled, bitlocker needs tpm",
2297        ),
2298        (
2299            "SMB / NTLM Security",
2300            "smb1 enabled, ntlmv1 in use, lan manager authentication",
2301        ),
2302        (
2303            "Windows Search",
2304            "windows search eating disk, search indexing not working",
2305        ),
2306        (
2307            "WMI",
2308            "wmi not working, wmi corrupt, winmgmt error",
2309        ),
2310    ]
2311}
2312
2313pub async fn generate_report_markdown() -> String {
2314    let timestamp = now_timestamp_string();
2315    let mut hostname = hostname_from_env();
2316    let version = env!("CARGO_PKG_VERSION");
2317    let mut sections: Vec<(&str, String)> = Vec::with_capacity(REPORT_TOPICS.len());
2318
2319    let total = REPORT_TOPICS.len();
2320    for (i, (topic, label)) in REPORT_TOPICS.iter().enumerate() {
2321        eprintln!("  [{}/{}] {}...", i + 1, total, label);
2322        let args = json!({"topic": topic});
2323        let output = match crate::tools::host_inspect::inspect_host(&args).await {
2324            Ok(s) => {
2325                if *topic == "hardware" {
2326                    for line in s.lines() {
2327                        let ll = line.to_ascii_lowercase();
2328                        if ll.contains("hostname") || ll.contains("computer name") {
2329                            if let Some(val) = line.split_once(':').map(|x| x.1) {
2330                                let h = val.trim().to_string();
2331                                if !h.is_empty() {
2332                                    hostname = h;
2333                                }
2334                            }
2335                        }
2336                    }
2337                }
2338                s
2339            }
2340            Err(e) => format!("Error: {}", e),
2341        };
2342        sections.push((label, output));
2343    }
2344
2345    let section_refs: Vec<(&str, &str)> = sections.iter().map(|(l, o)| (*l, o.as_str())).collect();
2346    let score = crate::agent::fix_recipes::score_health(&section_refs);
2347    let action_plan = crate::agent::fix_recipes::format_action_plan(&section_refs);
2348
2349    let mut md = String::with_capacity(action_plan.len() + sections.len() * 512 + 256);
2350    md.push_str("# Hematite Diagnostic Report\n\n");
2351    let _ = writeln!(md, "**Generated:** {}  ", timestamp);
2352    let _ = writeln!(md, "**Host:** {}  ", hostname);
2353    let _ = writeln!(md, "**Hematite:** v{}  ", version);
2354    let _ = write!(
2355        md,
2356        "**Health Score:** {} — {}  \n\n",
2357        score.grade, score.label
2358    );
2359    let _ = write!(md, "> {}\n\n", score.summary_line());
2360    md.push_str("---\n\n");
2361
2362    md.push_str("## Action Plan\n\n");
2363    md.push_str(&action_plan);
2364    md.push_str("---\n\n");
2365
2366    for (label, output) in &sections {
2367        let _ = write!(md, "## {}\n\n", label);
2368        md.push_str("```\n");
2369        md.push_str(output.trim_end());
2370        md.push_str("\n```\n\n");
2371    }
2372
2373    md
2374}
2375
2376struct DiagnosisData {
2377    timestamp: String,
2378    hostname: String,
2379    health_output: String,
2380    follow_up_outputs: Vec<(&'static str, String)>,
2381}
2382
2383async fn run_diagnosis_phases() -> DiagnosisData {
2384    let timestamp = now_timestamp_string();
2385    let hostname = hostname_from_env();
2386
2387    eprintln!("  → System Health (scanning for issues)...");
2388    let health_args = json!({"topic": "health_report"});
2389    let health_output = match crate::tools::host_inspect::inspect_host(&health_args).await {
2390        Ok(s) => s,
2391        Err(e) => format!("Error running health_report: {}", e),
2392    };
2393
2394    let follow_up_topics = crate::agent::diagnose::triage_follow_up_topics(&health_output);
2395
2396    if follow_up_topics.is_empty() {
2397        eprintln!("  → No follow-up checks needed.");
2398    } else {
2399        eprintln!(
2400            "  → {} area(s) flagged — running targeted checks...",
2401            follow_up_topics.len()
2402        );
2403    }
2404
2405    let mut follow_up_outputs: Vec<(&'static str, String)> =
2406        Vec::with_capacity(follow_up_topics.len());
2407    for (i, topic) in follow_up_topics.iter().enumerate() {
2408        eprintln!("  [{}/{}] {}...", i + 1, follow_up_topics.len(), topic);
2409        let args = json!({"topic": topic});
2410        let output = match crate::tools::host_inspect::inspect_host(&args).await {
2411            Ok(s) => s,
2412            Err(e) => format!("Error: {}", e),
2413        };
2414        follow_up_outputs.push((*topic, output));
2415    }
2416
2417    DiagnosisData {
2418        timestamp,
2419        hostname,
2420        health_output,
2421        follow_up_outputs,
2422    }
2423}
2424
2425/// Run a full staged diagnosis — health_report → triage → targeted follow-ups → fix recipes.
2426/// No TUI, no model required. Output is self-contained markdown for cloud model ingestion.
2427pub async fn generate_diagnosis_report() -> String {
2428    let version = env!("CARGO_PKG_VERSION");
2429    let data = run_diagnosis_phases().await;
2430
2431    let mut section_refs: Vec<(&str, &str)> = vec![("health_report", data.health_output.as_str())];
2432    for (topic, output) in &data.follow_up_outputs {
2433        section_refs.push((*topic, output.as_str()));
2434    }
2435    let score = crate::agent::fix_recipes::score_health(&section_refs);
2436    let action_plan = crate::agent::fix_recipes::format_action_plan(&section_refs);
2437
2438    let mut md =
2439        String::with_capacity(action_plan.len() + data.follow_up_outputs.len() * 512 + 256);
2440    md.push_str("# Hematite Staged Diagnosis Report\n\n");
2441    let _ = writeln!(md, "**Generated:** {}  ", data.timestamp);
2442    let _ = writeln!(md, "**Host:** {}  ", data.hostname);
2443    let _ = writeln!(md, "**Hematite:** v{}  ", version);
2444    let _ = write!(
2445        md,
2446        "**Health Score:** {} — {}  \n\n",
2447        score.grade, score.label
2448    );
2449    let _ = write!(md, "> {}\n\n", score.summary_line());
2450    md.push_str("---\n\n");
2451    md.push_str("## Action Plan\n\n");
2452    md.push_str(&action_plan);
2453    md.push_str("---\n\n");
2454    md.push_str("## System Health\n\n```\n");
2455    md.push_str(data.health_output.trim_end());
2456    md.push_str("\n```\n\n");
2457
2458    if !data.follow_up_outputs.is_empty() {
2459        md.push_str("## Targeted Investigation\n\n");
2460        for (topic, output) in &data.follow_up_outputs {
2461            let _ = write!(md, "### {}\n\n```\n", topic);
2462            md.push_str(output.trim_end());
2463            md.push_str("\n```\n\n");
2464        }
2465    }
2466
2467    md
2468}
2469
2470/// Same as generate_diagnosis_report but outputs a self-contained HTML file.
2471pub async fn generate_diagnosis_report_html() -> String {
2472    let version = env!("CARGO_PKG_VERSION");
2473    let data = run_diagnosis_phases().await;
2474
2475    let mut section_refs: Vec<(&str, &str)> = vec![("health_report", data.health_output.as_str())];
2476    for (topic, output) in &data.follow_up_outputs {
2477        section_refs.push((*topic, output.as_str()));
2478    }
2479    let score = crate::agent::fix_recipes::score_health(&section_refs);
2480    let action_plan_html = crate::agent::fix_recipes::format_action_plan_html(&section_refs);
2481
2482    let mut sections: Vec<(&str, String)> = vec![("System Health", data.health_output.clone())];
2483    for (topic, output) in &data.follow_up_outputs {
2484        sections.push((*topic, output.clone()));
2485    }
2486
2487    build_html_document(
2488        "Hematite Staged Diagnosis",
2489        &data.timestamp,
2490        &data.hostname,
2491        version,
2492        &score,
2493        &action_plan_html,
2494        &sections,
2495    )
2496}
2497
2498pub async fn generate_report_json() -> String {
2499    let timestamp = now_timestamp_string();
2500    let hostname = hostname_from_env();
2501    let version = env!("CARGO_PKG_VERSION");
2502    let mut obj = serde_json::Map::new();
2503    obj.insert("generated".into(), json!(timestamp));
2504    obj.insert("host".into(), json!(hostname));
2505    obj.insert("hematite_version".into(), json!(version));
2506
2507    let total = REPORT_TOPICS.len();
2508    for (i, (topic, label)) in REPORT_TOPICS.iter().enumerate() {
2509        eprintln!("  [{}/{}] {}...", i + 1, total, label);
2510        let args = json!({"topic": topic});
2511        let value = match crate::tools::host_inspect::inspect_host(&args).await {
2512            Ok(output) => json!({"label": label, "output": output}),
2513            Err(e) => json!({"label": label, "error": e}),
2514        };
2515        obj.insert(topic.to_string(), value);
2516    }
2517
2518    serde_json::to_string_pretty(&serde_json::Value::Object(obj))
2519        .unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e))
2520}
2521
2522/// Runs diagnostic topics, writes to `.hematite/reports/health-<timestamp>.md`,
2523/// and returns `(markdown_content, saved_path)`.
2524pub async fn save_report_markdown() -> (String, PathBuf) {
2525    let md = generate_report_markdown().await;
2526    let path = report_path("md");
2527    ensure_parent(&path);
2528    let _ = std::fs::write(&path, &md);
2529    (md, path)
2530}
2531
2532/// Same as `save_report_markdown` but JSON format.
2533pub async fn save_report_json() -> (String, PathBuf) {
2534    let json = generate_report_json().await;
2535    let path = report_path("json");
2536    ensure_parent(&path);
2537    let _ = std::fs::write(&path, &json);
2538    (json, path)
2539}
2540
2541/// Self-contained HTML diagnostic report — double-clickable, no external deps.
2542pub async fn generate_report_html() -> String {
2543    let timestamp = now_timestamp_string();
2544    let mut hostname = hostname_from_env();
2545    let version = env!("CARGO_PKG_VERSION");
2546    let mut sections: Vec<(&str, String)> = Vec::with_capacity(REPORT_TOPICS.len());
2547
2548    let total = REPORT_TOPICS.len();
2549    for (i, (topic, label)) in REPORT_TOPICS.iter().enumerate() {
2550        eprintln!("  [{}/{}] {}...", i + 1, total, label);
2551        let args = json!({"topic": topic});
2552        let output = match crate::tools::host_inspect::inspect_host(&args).await {
2553            Ok(s) => {
2554                if *topic == "hardware" {
2555                    for line in s.lines() {
2556                        let ll = line.to_ascii_lowercase();
2557                        if ll.contains("hostname") || ll.contains("computer name") {
2558                            if let Some(val) = line.split_once(':').map(|x| x.1) {
2559                                let h = val.trim().to_string();
2560                                if !h.is_empty() {
2561                                    hostname = h;
2562                                }
2563                            }
2564                        }
2565                    }
2566                }
2567                s
2568            }
2569            Err(e) => format!("Error: {}", e),
2570        };
2571        sections.push((label, output));
2572    }
2573
2574    let section_refs: Vec<(&str, &str)> = sections.iter().map(|(l, o)| (*l, o.as_str())).collect();
2575    let score = crate::agent::fix_recipes::score_health(&section_refs);
2576    let action_plan_html = crate::agent::fix_recipes::format_action_plan_html(&section_refs);
2577
2578    build_html_document(
2579        "Hematite Diagnostic Report",
2580        &timestamp,
2581        &hostname,
2582        version,
2583        &score,
2584        &action_plan_html,
2585        &sections,
2586    )
2587}
2588
2589pub async fn save_report_html() -> (String, PathBuf) {
2590    let html = generate_report_html().await;
2591    let path = report_path("html");
2592    ensure_parent(&path);
2593    let _ = std::fs::write(&path, &html);
2594    (html, path)
2595}
2596
2597pub async fn save_diagnosis_report() -> (String, PathBuf) {
2598    let md = generate_diagnosis_report().await;
2599    let path = crate::tools::file_ops::hematite_dir()
2600        .join("reports")
2601        .join(format!("diagnosis-{}.md", now_file_timestamp()));
2602    ensure_parent(&path);
2603    let _ = std::fs::write(&path, &md);
2604    (md, path)
2605}
2606
2607pub async fn save_diagnosis_report_html() -> (String, PathBuf) {
2608    let html = generate_diagnosis_report_html().await;
2609    let path = crate::tools::file_ops::hematite_dir()
2610        .join("reports")
2611        .join(format!("diagnosis-{}.html", now_file_timestamp()));
2612    ensure_parent(&path);
2613    let _ = std::fs::write(&path, &html);
2614    (html, path)
2615}
2616
2617pub async fn generate_diagnosis_report_json() -> String {
2618    let version = env!("CARGO_PKG_VERSION");
2619    let data = run_diagnosis_phases().await;
2620    let mut section_refs: Vec<(&str, &str)> = vec![("health_report", data.health_output.as_str())];
2621    for (topic, output) in &data.follow_up_outputs {
2622        section_refs.push((*topic, output.as_str()));
2623    }
2624    let score = crate::agent::fix_recipes::score_health(&section_refs);
2625
2626    // Collect action/investigate titles by scanning each section independently
2627    let mut seen_titles = std::collections::HashSet::new();
2628    let mut action_items: Vec<&str> = Vec::new();
2629    let mut investigate_items: Vec<&str> = Vec::new();
2630    for (_label, output) in &section_refs {
2631        for recipe in crate::agent::fix_recipes::match_recipes(output) {
2632            if seen_titles.insert(recipe.title) {
2633                match recipe.severity {
2634                    "ACTION" => action_items.push(recipe.title),
2635                    "INVESTIGATE" => investigate_items.push(recipe.title),
2636                    _ => {}
2637                }
2638            }
2639        }
2640    }
2641
2642    let mut sections_obj = serde_json::Map::new();
2643    sections_obj.insert("health_report".into(), json!(data.health_output));
2644    for (topic, output) in &data.follow_up_outputs {
2645        sections_obj.insert(topic.to_string(), json!(output));
2646    }
2647
2648    let obj = json!({
2649        "generated": data.timestamp,
2650        "host": data.hostname,
2651        "hematite_version": version,
2652        "grade": score.grade.to_string(),
2653        "label": score.label,
2654        "action_count": score.action_count,
2655        "investigate_count": score.investigate_count,
2656        "monitor_count": score.monitor_count,
2657        "action_items": action_items,
2658        "investigate_items": investigate_items,
2659        "follow_up_topics": data.follow_up_outputs.iter().map(|(t, _)| *t).collect::<Vec<_>>(),
2660        "sections": serde_json::Value::Object(sections_obj),
2661    });
2662    serde_json::to_string_pretty(&obj).unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e))
2663}
2664
2665pub async fn save_diagnosis_report_json() -> (String, PathBuf) {
2666    let json = generate_diagnosis_report_json().await;
2667    let path = crate::tools::file_ops::hematite_dir()
2668        .join("reports")
2669        .join(format!("diagnosis-{}.json", now_file_timestamp()));
2670    ensure_parent(&path);
2671    let _ = std::fs::write(&path, &json);
2672    (json, path)
2673}
2674
2675fn build_html_document(
2676    title: &str,
2677    timestamp: &str,
2678    hostname: &str,
2679    version: &str,
2680    score: &crate::agent::fix_recipes::HealthScore,
2681    action_plan_html: &str,
2682    sections: &[(&str, String)],
2683) -> String {
2684    use crate::agent::html_template::{build_html_shell, he, COPY_BUTTON_HTML};
2685
2686    let mut sections_html =
2687        String::with_capacity(sections.iter().map(|(_, o)| o.len() + 64).sum::<usize>());
2688    for (label, output) in sections {
2689        let _ = writeln!(
2690            sections_html,
2691            "<details><summary>{}</summary><pre>{}</pre></details>",
2692            he(label),
2693            he(output.trim_end())
2694        );
2695    }
2696
2697    let content = format!(
2698        r#"<header>
2699<h1>{title}</h1>
2700<div class="meta">
2701  <span>Generated: {timestamp}</span>
2702  <span>Host: {hostname}</span>
2703  <span>Hematite v{version}</span>
2704</div>
2705<div class="score-row">
2706  <div class="grade g{grade}">{grade}</div>
2707  <div class="score-info">
2708    <h2>Health Score: {grade} — {label}</h2>
2709    <p>{summary}</p>
2710  </div>
2711</div>
2712<p class="grade-intro">{intro}</p>
2713{copy_btn}
2714</header>
2715<section>
2716<h2>Action Plan</h2>
2717{action_plan_html}
2718</section>
2719<section>
2720<h2>Diagnostic Data</h2>
2721{sections_html}
2722</section>"#,
2723        title = he(title),
2724        hostname = he(hostname),
2725        timestamp = he(timestamp),
2726        version = he(version),
2727        grade = score.grade,
2728        label = he(score.label),
2729        summary = he(&score.summary_line()),
2730        intro = he(score.grade_intro()),
2731        copy_btn = COPY_BUTTON_HTML,
2732        action_plan_html = action_plan_html,
2733        sections_html = sections_html,
2734    );
2735
2736    let page_title = format!("{} — {}", he(title), he(hostname));
2737    build_html_shell(&page_title, version, &content)
2738}
2739
2740// ── Triage report (IT-first-look, no model required) ─────────────────────────
2741
2742struct TriageData {
2743    timestamp: String,
2744    hostname: String,
2745    sections: Vec<(&'static str, String)>,
2746}
2747
2748async fn run_triage_phases(preset: &str) -> TriageData {
2749    let topics = triage_topics_for_preset(preset);
2750    let total = topics.len();
2751    let timestamp = now_timestamp_string();
2752    let mut hostname = hostname_from_env();
2753    let mut sections: Vec<(&'static str, String)> = Vec::with_capacity(total);
2754
2755    for (i, &(topic, label)) in topics.iter().enumerate() {
2756        eprintln!("  [{}/{}] {}...", i + 1, total, label);
2757        let args = serde_json::json!({"topic": topic});
2758        let output = match crate::tools::host_inspect::inspect_host(&args).await {
2759            Ok(s) => {
2760                if topic == "health_report" {
2761                    for line in s.lines() {
2762                        let ll = line.to_ascii_lowercase();
2763                        if ll.contains("hostname") || ll.contains("computer name") {
2764                            if let Some(val) = line.split_once(':').map(|x| x.1) {
2765                                let h = val.trim().to_string();
2766                                if !h.is_empty() {
2767                                    hostname = h;
2768                                }
2769                            }
2770                        }
2771                    }
2772                }
2773                s
2774            }
2775            Err(e) => format!("Error: {}", e),
2776        };
2777        sections.push((label, output));
2778    }
2779
2780    TriageData {
2781        timestamp,
2782        hostname,
2783        sections,
2784    }
2785}
2786
2787pub async fn generate_triage_report_markdown(preset: &str) -> String {
2788    let title = triage_preset_title(preset);
2789    let data = run_triage_phases(preset).await;
2790    let version = env!("CARGO_PKG_VERSION");
2791
2792    let section_refs: Vec<(&str, &str)> = data
2793        .sections
2794        .iter()
2795        .map(|(l, o)| (*l, o.as_str()))
2796        .collect();
2797    let score = crate::agent::fix_recipes::score_health(&section_refs);
2798    let action_plan = crate::agent::fix_recipes::format_action_plan(&section_refs);
2799
2800    let mut md = String::with_capacity(action_plan.len() + data.sections.len() * 512 + 256);
2801    let _ = write!(md, "# {}\n\n", title);
2802    let _ = writeln!(md, "**Generated:** {}  ", data.timestamp);
2803    let _ = writeln!(md, "**Host:** {}  ", data.hostname);
2804    let _ = writeln!(md, "**Hematite:** v{}  ", version);
2805    let _ = write!(
2806        md,
2807        "**Health Score:** {} — {}  \n\n",
2808        score.grade, score.label
2809    );
2810    let _ = write!(md, "> {}\n\n", score.summary_line());
2811    md.push_str("---\n\n## Action Plan\n\n");
2812    md.push_str(&action_plan);
2813    md.push_str("---\n\n");
2814    for (label, output) in &data.sections {
2815        let _ = write!(md, "## {}\n\n```\n", label);
2816        md.push_str(output.trim_end());
2817        md.push_str("\n```\n\n");
2818    }
2819    md
2820}
2821
2822pub async fn generate_triage_report_html(preset: &str) -> String {
2823    let title = triage_preset_title(preset);
2824    let data = run_triage_phases(preset).await;
2825    let version = env!("CARGO_PKG_VERSION");
2826
2827    let section_refs: Vec<(&str, &str)> = data
2828        .sections
2829        .iter()
2830        .map(|(l, o)| (*l, o.as_str()))
2831        .collect();
2832    let score = crate::agent::fix_recipes::score_health(&section_refs);
2833    let action_plan_html = crate::agent::fix_recipes::format_action_plan_html(&section_refs);
2834
2835    build_html_document(
2836        title,
2837        &data.timestamp,
2838        &data.hostname,
2839        version,
2840        &score,
2841        &action_plan_html,
2842        &data.sections,
2843    )
2844}
2845
2846pub async fn save_triage_report(preset: &str) -> (String, PathBuf) {
2847    let md = generate_triage_report_markdown(preset).await;
2848    let path = crate::tools::file_ops::hematite_dir()
2849        .join("reports")
2850        .join(format!("triage-{}.md", now_file_timestamp()));
2851    ensure_parent(&path);
2852    let _ = std::fs::write(&path, &md);
2853    (md, path)
2854}
2855
2856pub async fn save_triage_report_html(preset: &str) -> (String, PathBuf) {
2857    let html = generate_triage_report_html(preset).await;
2858    let path = crate::tools::file_ops::hematite_dir()
2859        .join("reports")
2860        .join(format!("triage-{}.html", now_file_timestamp()));
2861    ensure_parent(&path);
2862    let _ = std::fs::write(&path, &html);
2863    (html, path)
2864}
2865
2866pub async fn generate_triage_report_json(preset: &str) -> String {
2867    let data = run_triage_phases(preset).await;
2868    let version = env!("CARGO_PKG_VERSION");
2869    let section_refs: Vec<(&str, &str)> = data
2870        .sections
2871        .iter()
2872        .map(|(l, o)| (*l, o.as_str()))
2873        .collect();
2874    let score = crate::agent::fix_recipes::score_health(&section_refs);
2875
2876    let mut seen_titles = std::collections::HashSet::new();
2877    let mut action_items: Vec<&str> = Vec::new();
2878    let mut investigate_items: Vec<&str> = Vec::new();
2879    for (_label, output) in &section_refs {
2880        for recipe in crate::agent::fix_recipes::match_recipes(output) {
2881            if seen_titles.insert(recipe.title) {
2882                match recipe.severity {
2883                    "ACTION" => action_items.push(recipe.title),
2884                    "INVESTIGATE" => investigate_items.push(recipe.title),
2885                    _ => {}
2886                }
2887            }
2888        }
2889    }
2890
2891    let mut sections_obj = serde_json::Map::new();
2892    for (label, output) in &data.sections {
2893        sections_obj.insert(label.to_string(), json!(output));
2894    }
2895
2896    let obj = json!({
2897        "generated": data.timestamp,
2898        "host": data.hostname,
2899        "hematite_version": version,
2900        "preset": preset,
2901        "grade": score.grade.to_string(),
2902        "label": score.label,
2903        "action_count": score.action_count,
2904        "investigate_count": score.investigate_count,
2905        "monitor_count": score.monitor_count,
2906        "action_items": action_items,
2907        "investigate_items": investigate_items,
2908        "sections": serde_json::Value::Object(sections_obj),
2909    });
2910    serde_json::to_string_pretty(&obj).unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e))
2911}
2912
2913pub async fn save_triage_report_json(preset: &str) -> (String, PathBuf) {
2914    let json = generate_triage_report_json(preset).await;
2915    let path = crate::tools::file_ops::hematite_dir()
2916        .join("reports")
2917        .join(format!("triage-{}.json", now_file_timestamp()));
2918    ensure_parent(&path);
2919    let _ = std::fs::write(&path, &json);
2920    (json, path)
2921}
2922
2923// ── Fix Plan (--fix "<issue>", no model required) ─────────────────────────────
2924
2925struct FixPlanData {
2926    timestamp: String,
2927    hostname: String,
2928    sections: Vec<(&'static str, String)>,
2929}
2930
2931async fn run_fix_plan_phases(issue: &str) -> FixPlanData {
2932    let initial_topics = topics_for_issue(issue);
2933    let total = initial_topics.len();
2934    let timestamp = now_timestamp_string();
2935    let mut hostname = hostname_from_env();
2936    let mut sections: Vec<(&'static str, String)> = Vec::with_capacity(total);
2937
2938    eprintln!("hematite --fix: \"{}\" ({} check(s))", issue, total);
2939    for (i, &(topic, label)) in initial_topics.iter().enumerate() {
2940        eprintln!("  [{}/{}] {}...", i + 1, total, label);
2941        let args = serde_json::json!({"topic": topic});
2942        let output = match crate::tools::host_inspect::inspect_host(&args).await {
2943            Ok(s) => {
2944                if topic == "health_report" {
2945                    for line in s.lines() {
2946                        let ll = line.to_ascii_lowercase();
2947                        if ll.contains("hostname") || ll.contains("computer name") {
2948                            if let Some(val) = line.split_once(':').map(|x| x.1) {
2949                                let h = val.trim().to_string();
2950                                if !h.is_empty() {
2951                                    hostname = h;
2952                                }
2953                            }
2954                        }
2955                    }
2956                }
2957                s
2958            }
2959            Err(e) => format!("Error: {}", e),
2960        };
2961        sections.push((label, output));
2962    }
2963
2964    let combined: String = {
2965        let total = sections.iter().map(|(_, o)| o.len()).sum::<usize>() + sections.len();
2966        let mut s = String::with_capacity(total);
2967        for (i, (_, o)) in sections.iter().enumerate() {
2968            if i > 0 {
2969                s.push('\n');
2970            }
2971            s.push_str(o);
2972        }
2973        s
2974    };
2975    let ran: Vec<&str> = initial_topics.iter().map(|&(t, _)| t).collect();
2976    let follow_ups = crate::agent::diagnose::fix_follow_up_topics(&combined, &ran);
2977
2978    if !follow_ups.is_empty() {
2979        eprintln!(
2980            "  → {} follow-up check(s) triggered by findings...",
2981            follow_ups.len()
2982        );
2983    }
2984
2985    for (i, &(topic, label)) in follow_ups.iter().enumerate() {
2986        eprintln!("  + [{}/{}] {}...", i + 1, follow_ups.len(), label);
2987        let args = serde_json::json!({"topic": topic});
2988        let output = match crate::tools::host_inspect::inspect_host(&args).await {
2989            Ok(s) => s,
2990            Err(e) => format!("Error: {}", e),
2991        };
2992        sections.push((label, output));
2993    }
2994
2995    FixPlanData {
2996        timestamp,
2997        hostname,
2998        sections,
2999    }
3000}
3001
3002pub async fn generate_fix_plan_markdown(issue: &str) -> String {
3003    let data = run_fix_plan_phases(issue).await;
3004    let version = env!("CARGO_PKG_VERSION");
3005
3006    let section_refs: Vec<(&str, &str)> = data
3007        .sections
3008        .iter()
3009        .map(|(l, o)| (*l, o.as_str()))
3010        .collect();
3011    let score = crate::agent::fix_recipes::score_health(&section_refs);
3012    let action_plan = crate::agent::fix_recipes::format_action_plan(&section_refs);
3013
3014    let mut md = String::with_capacity(action_plan.len() + data.sections.len() * 512 + 256);
3015    md.push_str("# Hematite Fix Plan\n\n");
3016    let _ = writeln!(md, "**Issue:** {}  ", issue);
3017    let _ = writeln!(md, "**Generated:** {}  ", data.timestamp);
3018    let _ = writeln!(md, "**Host:** {}  ", data.hostname);
3019    let _ = writeln!(md, "**Hematite:** v{}  ", version);
3020    let _ = write!(
3021        md,
3022        "**Health Score:** {} — {}  \n\n",
3023        score.grade, score.label
3024    );
3025    let _ = write!(md, "> {}\n\n", score.summary_line());
3026    md.push_str("---\n\n## Fix Steps\n\n");
3027    md.push_str(&action_plan);
3028    md.push_str("---\n\n");
3029    for (label, output) in &data.sections {
3030        let _ = write!(md, "## {}\n\n```\n", label);
3031        md.push_str(output.trim_end());
3032        md.push_str("\n```\n\n");
3033    }
3034    md
3035}
3036
3037pub async fn generate_fix_plan_html(issue: &str) -> String {
3038    let data = run_fix_plan_phases(issue).await;
3039    let version = env!("CARGO_PKG_VERSION");
3040
3041    let section_refs: Vec<(&str, &str)> = data
3042        .sections
3043        .iter()
3044        .map(|(l, o)| (*l, o.as_str()))
3045        .collect();
3046    let score = crate::agent::fix_recipes::score_health(&section_refs);
3047    let action_plan_html = crate::agent::fix_recipes::format_action_plan_html(&section_refs);
3048
3049    use crate::agent::html_template::{build_html_shell, he, COPY_BUTTON_HTML};
3050
3051    let mut sections_html = String::with_capacity(data.sections.len() * 512);
3052    for (label, output) in &data.sections {
3053        let _ = writeln!(
3054            sections_html,
3055            "<details><summary>{}</summary><pre>{}</pre></details>",
3056            he(label),
3057            he(output.trim_end())
3058        );
3059    }
3060
3061    let content = format!(
3062        r#"<header>
3063<h1>Fix Plan</h1>
3064<p class="grade-intro" style="margin-bottom:.85rem">Issue: <strong>{issue}</strong></p>
3065<div class="meta">
3066  <span>Generated: {timestamp}</span>
3067  <span>Host: {hostname}</span>
3068  <span>Hematite v{version}</span>
3069</div>
3070<div class="score-row">
3071  <div class="grade g{grade}">{grade}</div>
3072  <div class="score-info">
3073    <h2>Health Score: {grade} — {label}</h2>
3074    <p>{summary}</p>
3075  </div>
3076</div>
3077{copy_btn}
3078</header>
3079<section>
3080<h2>Fix Steps</h2>
3081{action_plan_html}
3082</section>
3083<section>
3084<h2>Diagnostic Data</h2>
3085{sections_html}
3086</section>"#,
3087        issue = he(issue),
3088        hostname = he(&data.hostname),
3089        timestamp = he(&data.timestamp),
3090        version = he(version),
3091        grade = score.grade,
3092        label = he(score.label),
3093        summary = he(&score.summary_line()),
3094        copy_btn = COPY_BUTTON_HTML,
3095        action_plan_html = action_plan_html,
3096        sections_html = sections_html,
3097    );
3098
3099    let page_title = format!("Fix Plan: {} — {}", he(issue), he(&data.hostname));
3100    build_html_shell(&page_title, version, &content)
3101}
3102
3103pub async fn save_fix_plan(issue: &str) -> (String, PathBuf) {
3104    let md = generate_fix_plan_markdown(issue).await;
3105    let path = crate::tools::file_ops::hematite_dir()
3106        .join("reports")
3107        .join(format!("fix-{}.md", now_file_timestamp()));
3108    ensure_parent(&path);
3109    let _ = std::fs::write(&path, &md);
3110    (md, path)
3111}
3112
3113/// Like `save_fix_plan` but also returns the plain-text action plan for immediate
3114/// terminal printing. Returns `(action_plan_text, full_md, path)`.
3115pub async fn save_fix_plan_with_summary(issue: &str) -> (String, String, PathBuf) {
3116    let data = run_fix_plan_phases(issue).await;
3117    let version = env!("CARGO_PKG_VERSION");
3118
3119    let section_refs: Vec<(&str, &str)> = data
3120        .sections
3121        .iter()
3122        .map(|(l, o)| (*l, o.as_str()))
3123        .collect();
3124    let score = crate::agent::fix_recipes::score_health(&section_refs);
3125    let action_plan = crate::agent::fix_recipes::format_action_plan(&section_refs);
3126
3127    let mut md = String::with_capacity(action_plan.len() + data.sections.len() * 512 + 256);
3128    md.push_str("# Hematite Fix Plan\n\n");
3129    let _ = writeln!(md, "**Issue:** {}  ", issue);
3130    let _ = writeln!(md, "**Generated:** {}  ", data.timestamp);
3131    let _ = writeln!(md, "**Host:** {}  ", data.hostname);
3132    let _ = writeln!(md, "**Hematite:** v{}  ", version);
3133    let _ = write!(
3134        md,
3135        "**Health Score:** {} — {}  \n\n",
3136        score.grade, score.label
3137    );
3138    let _ = write!(md, "> {}\n\n", score.summary_line());
3139    md.push_str("---\n\n## Fix Steps\n\n");
3140    md.push_str(&action_plan);
3141    md.push_str("---\n\n");
3142    for (label, output) in &data.sections {
3143        let _ = write!(md, "## {}\n\n```\n", label);
3144        md.push_str(output.trim_end());
3145        md.push_str("\n```\n\n");
3146    }
3147
3148    let path = crate::tools::file_ops::hematite_dir()
3149        .join("reports")
3150        .join(format!("fix-{}.md", now_file_timestamp()));
3151    ensure_parent(&path);
3152    let _ = std::fs::write(&path, &md);
3153
3154    let summary = format!(
3155        "Health Score: {} — {}\n\n{}",
3156        score.grade, score.label, action_plan
3157    );
3158    (summary, md, path)
3159}
3160
3161pub async fn save_fix_plan_html(issue: &str) -> (String, PathBuf) {
3162    let html = generate_fix_plan_html(issue).await;
3163    let path = crate::tools::file_ops::hematite_dir()
3164        .join("reports")
3165        .join(format!("fix-{}.html", now_file_timestamp()));
3166    ensure_parent(&path);
3167    let _ = std::fs::write(&path, &html);
3168    (html, path)
3169}
3170
3171pub async fn save_fix_plan_json(issue: &str) -> (String, PathBuf) {
3172    let data = run_fix_plan_phases(issue).await;
3173    let version = env!("CARGO_PKG_VERSION");
3174    let section_refs: Vec<(&str, &str)> = data
3175        .sections
3176        .iter()
3177        .map(|(l, o)| (*l, o.as_str()))
3178        .collect();
3179    let score = crate::agent::fix_recipes::score_health(&section_refs);
3180    let action_plan = crate::agent::fix_recipes::format_action_plan(&section_refs);
3181
3182    let mut seen_titles = std::collections::HashSet::new();
3183    let mut action_items: Vec<&str> = Vec::new();
3184    let mut investigate_items: Vec<&str> = Vec::new();
3185    for (_label, output) in &section_refs {
3186        for recipe in crate::agent::fix_recipes::match_recipes(output) {
3187            if seen_titles.insert(recipe.title) {
3188                match recipe.severity {
3189                    "ACTION" => action_items.push(recipe.title),
3190                    "INVESTIGATE" => investigate_items.push(recipe.title),
3191                    _ => {}
3192                }
3193            }
3194        }
3195    }
3196
3197    let mut sections_obj = serde_json::Map::new();
3198    for (label, output) in &data.sections {
3199        sections_obj.insert(label.to_string(), json!(output));
3200    }
3201
3202    let obj = json!({
3203        "generated": data.timestamp,
3204        "host": data.hostname,
3205        "hematite_version": version,
3206        "issue": issue,
3207        "grade": score.grade.to_string(),
3208        "label": score.label,
3209        "action_count": score.action_count,
3210        "investigate_count": score.investigate_count,
3211        "action_items": action_items,
3212        "investigate_items": investigate_items,
3213        "action_plan": action_plan,
3214        "sections": serde_json::Value::Object(sections_obj),
3215    });
3216    let json_str =
3217        serde_json::to_string_pretty(&obj).unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e));
3218    let path = crate::tools::file_ops::hematite_dir()
3219        .join("reports")
3220        .join(format!("fix-{}.json", now_file_timestamp()));
3221    ensure_parent(&path);
3222    let _ = std::fs::write(&path, &json_str);
3223    (json_str, path)
3224}
3225
3226// ── Direct topic inspection (--inspect, /query) ──────────────────────────────
3227
3228/// Run one or more inspect_host topics by name and return combined plain-text output.
3229/// `topics_csv` is a comma-separated list: e.g. "wifi,latency,dns_cache".
3230pub async fn generate_inspect_output(topics_csv: &str) -> String {
3231    let topics: Vec<&str> = topics_csv
3232        .split(',')
3233        .map(str::trim)
3234        .filter(|s| !s.is_empty())
3235        .collect();
3236
3237    if topics.is_empty() {
3238        return "No topics specified. Example: hematite --inspect wifi,latency,dns_cache\n\
3239                Run `hematite --inventory` to list all 128+ available topics.\n"
3240            .to_string();
3241    }
3242
3243    let total = topics.len();
3244    if total > 1 {
3245        eprintln!("hematite --inspect: {} topic(s)", total);
3246    }
3247    let mut out = String::new();
3248    for (i, topic) in topics.iter().enumerate() {
3249        if total > 1 {
3250            eprintln!("  [{}/{}] {}...", i + 1, total, topic);
3251        }
3252        let args = json!({"topic": topic});
3253        let result = match crate::tools::host_inspect::inspect_host(&args).await {
3254            Ok(s) => s,
3255            Err(e) => format!("Error ({}): {}", topic, e),
3256        };
3257        if total > 1 {
3258            let _ = writeln!(out, "─── {} ───", topic);
3259        }
3260        out.push_str(result.trim_end());
3261        out.push('\n');
3262        if total > 1 {
3263            out.push('\n');
3264        }
3265    }
3266    out
3267}
3268
3269/// Run one or more inspect_host topics and return structured JSON output.
3270pub async fn generate_inspect_output_json(topics_csv: &str) -> String {
3271    let topics: Vec<&str> = topics_csv
3272        .split(',')
3273        .map(str::trim)
3274        .filter(|s| !s.is_empty())
3275        .collect();
3276
3277    if topics.is_empty() {
3278        return json!({"error": "No topics specified."}).to_string();
3279    }
3280
3281    let total = topics.len();
3282    eprintln!("hematite --inspect (json): {} topic(s)", total);
3283    let mut sections_obj = serde_json::Map::new();
3284    let mut combined = String::new();
3285    for (i, topic) in topics.iter().enumerate() {
3286        eprintln!("  [{}/{}] {}...", i + 1, total, topic);
3287        let args = json!({"topic": topic});
3288        let result = match crate::tools::host_inspect::inspect_host(&args).await {
3289            Ok(s) => s,
3290            Err(e) => format!("Error ({}): {}", topic, e),
3291        };
3292        sections_obj.insert(topic.to_string(), json!(result));
3293        combined.push_str(result.trim_end());
3294        combined.push('\n');
3295    }
3296
3297    let host = std::env::var("COMPUTERNAME")
3298        .or_else(|_| std::env::var("HOSTNAME"))
3299        .unwrap_or_else(|_| "unknown".to_string());
3300    let obj = json!({
3301        "generated": now_timestamp_string(),
3302        "host": host,
3303        "hematite_version": env!("CARGO_PKG_VERSION"),
3304        "topics": topics,
3305        "sections": serde_json::Value::Object(sections_obj),
3306        "combined_output": combined,
3307    });
3308    serde_json::to_string_pretty(&obj).unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e))
3309}
3310
3311/// Run inspect topics and optionally save as a report file.
3312/// Returns `(content, saved_path_option)`.
3313pub async fn run_inspect_topics(
3314    topics_csv: &str,
3315    fmt: &str,
3316    save: bool,
3317) -> (String, Option<PathBuf>) {
3318    let content = if fmt == "json" {
3319        generate_inspect_output_json(topics_csv).await
3320    } else {
3321        generate_inspect_output(topics_csv).await
3322    };
3323
3324    if !save {
3325        return (content, None);
3326    }
3327
3328    let path = crate::tools::file_ops::hematite_dir()
3329        .join("reports")
3330        .join(format!("inspect-{}.{}", now_file_timestamp(), fmt));
3331    ensure_parent(&path);
3332    let _ = std::fs::write(&path, &content);
3333    (content, Some(path))
3334}
3335
3336/// Route a natural-language query to the appropriate inspect_host topics and run them.
3337/// Uses `all_host_inspection_topics()` for multi-topic detection, falls back to
3338/// `preferred_host_inspection_topic()` for single-topic, then "summary" if nothing matches.
3339pub async fn generate_query_output(query: &str) -> String {
3340    use crate::agent::routing::{all_host_inspection_topics, preferred_host_inspection_topic};
3341
3342    let detected = all_host_inspection_topics(query);
3343    let topics: Vec<&str> = if !detected.is_empty() {
3344        detected
3345    } else {
3346        match preferred_host_inspection_topic(query) {
3347            Some(t) => vec![t],
3348            None => vec!["summary"],
3349        }
3350    };
3351
3352    let total = topics.len();
3353    eprintln!("hematite --query: {} topic(s) matched", total);
3354    let mut out = String::new();
3355    for (i, topic) in topics.iter().enumerate() {
3356        eprintln!("  [{}/{}] {}...", i + 1, total, topic);
3357        let args = json!({"topic": topic});
3358        let result = match crate::tools::host_inspect::inspect_host(&args).await {
3359            Ok(s) => s,
3360            Err(e) => format!("Error ({}): {}", topic, e),
3361        };
3362        if total > 1 {
3363            let _ = writeln!(out, "─── {} ───", topic);
3364        }
3365        out.push_str(result.trim_end());
3366        out.push('\n');
3367        if total > 1 {
3368            out.push('\n');
3369        }
3370    }
3371    out
3372}
3373
3374/// Like `generate_query_output` but returns structured JSON with matched topics + per-topic output.
3375pub async fn generate_query_output_json(query: &str) -> String {
3376    use crate::agent::routing::{all_host_inspection_topics, preferred_host_inspection_topic};
3377
3378    let detected = all_host_inspection_topics(query);
3379    let topics: Vec<&str> = if !detected.is_empty() {
3380        detected
3381    } else {
3382        match preferred_host_inspection_topic(query) {
3383            Some(t) => vec![t],
3384            None => vec!["summary"],
3385        }
3386    };
3387
3388    let total = topics.len();
3389    eprintln!("hematite --query (json): {} topic(s) matched", total);
3390    let mut sections_obj = serde_json::Map::new();
3391    let mut combined = String::new();
3392    for (i, topic) in topics.iter().enumerate() {
3393        eprintln!("  [{}/{}] {}...", i + 1, total, topic);
3394        let args = json!({"topic": topic});
3395        let result = match crate::tools::host_inspect::inspect_host(&args).await {
3396            Ok(s) => s,
3397            Err(e) => format!("Error ({}): {}", topic, e),
3398        };
3399        sections_obj.insert(topic.to_string(), json!(result));
3400        combined.push_str(result.trim_end());
3401        combined.push('\n');
3402    }
3403
3404    let obj = json!({
3405        "query": query,
3406        "matched_topics": topics,
3407        "sections": serde_json::Value::Object(sections_obj),
3408        "combined_output": combined,
3409    });
3410    serde_json::to_string_pretty(&obj).unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e))
3411}
3412
3413/// Save arbitrary markdown content as a dark-theme HTML page.
3414/// Returns `(html_string, saved_path)`. Title defaults to a timestamp slug
3415/// if empty. Saves to `.hematite/reports/research-DATE.html`.
3416pub fn save_research_html(title: &str, body_md: &str) -> (String, PathBuf) {
3417    use crate::agent::html_template::{build_html_shell, he, markdown_to_html, COPY_BUTTON_HTML};
3418    let version = env!("CARGO_PKG_VERSION");
3419    let timestamp = now_timestamp_string();
3420    let display_title = if title.trim().is_empty() {
3421        format!("Research — {}", &timestamp[..10])
3422    } else {
3423        title.to_string()
3424    };
3425
3426    let body_html = markdown_to_html(body_md);
3427    let content = format!(
3428        r#"<header>
3429<h1>{title}</h1>
3430<div class="meta">
3431  <span>Saved: {timestamp}</span>
3432  <span>Hematite v{version}</span>
3433</div>
3434{copy_btn}
3435</header>
3436<section>
3437{body_html}
3438</section>"#,
3439        title = he(&display_title),
3440        timestamp = he(&timestamp),
3441        version = he(version),
3442        copy_btn = COPY_BUTTON_HTML,
3443        body_html = body_html,
3444    );
3445
3446    let html = build_html_shell(&display_title, version, &content);
3447    let path = crate::tools::file_ops::hematite_dir()
3448        .join("reports")
3449        .join(format!("research-{}.html", now_file_timestamp()));
3450    ensure_parent(&path);
3451    let _ = std::fs::write(&path, &html);
3452    (html, path)
3453}
3454
3455fn report_path(ext: &str) -> PathBuf {
3456    crate::tools::file_ops::hematite_dir()
3457        .join("reports")
3458        .join(format!("health-{}.{}", now_file_timestamp(), ext))
3459}
3460
3461fn ensure_parent(path: &Path) {
3462    if let Some(parent) = path.parent() {
3463        let _ = std::fs::create_dir_all(parent);
3464    }
3465}
3466
3467pub fn timestamp_label() -> String {
3468    now_timestamp_string()
3469}
3470
3471fn now_timestamp_string() -> String {
3472    let now = unix_now();
3473    let (y, mo, d, h, mi, s) = epoch_to_ymd_hms(now);
3474    format!(
3475        "{:04}-{:02}-{:02} {:02}:{:02}:{:02} UTC",
3476        y, mo, d, h, mi, s
3477    )
3478}
3479
3480fn now_file_timestamp() -> String {
3481    let now = unix_now();
3482    let (y, mo, d, h, mi, _s) = epoch_to_ymd_hms(now);
3483    format!("{:04}-{:02}-{:02}_{:02}-{:02}", y, mo, d, h, mi)
3484}
3485
3486fn unix_now() -> u64 {
3487    std::time::SystemTime::now()
3488        .duration_since(std::time::UNIX_EPOCH)
3489        .unwrap_or_default()
3490        .as_secs()
3491}
3492
3493fn hostname_from_env() -> String {
3494    std::env::var("COMPUTERNAME")
3495        .or_else(|_| std::env::var("HOSTNAME"))
3496        .unwrap_or_else(|_| "unknown".to_string())
3497}
3498
3499/// Gregorian calendar decomposition of a Unix timestamp (accurate 1970–2100).
3500fn epoch_to_ymd_hms(epoch: u64) -> (u32, u32, u32, u32, u32, u32) {
3501    let s = (epoch % 60) as u32;
3502    let mi = ((epoch / 60) % 60) as u32;
3503    let h = ((epoch / 3600) % 24) as u32;
3504    let days = epoch / 86400;
3505
3506    let years_400 = days / 146097;
3507    let rem = days % 146097;
3508    let years_100 = rem.min(146096) / 36524;
3509    let rem = rem - years_100 * 36524;
3510    let years_4 = rem / 1461;
3511    let rem = rem % 1461;
3512    let years_1 = rem.min(1460) / 365;
3513    let rem = rem - years_1 * 365;
3514
3515    let year = (1970 + years_400 * 400 + years_100 * 100 + years_4 * 4 + years_1) as u32;
3516    let leap = u32::from(
3517        year.is_multiple_of(4) && (!year.is_multiple_of(100) || year.is_multiple_of(400)),
3518    );
3519    let month_days: [u32; 12] = [31, 28 + leap, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
3520    let mut rem = rem as u32;
3521    let mut month = 1u32;
3522    for &md in &month_days {
3523        if rem < md {
3524            break;
3525        }
3526        rem -= md;
3527        month += 1;
3528    }
3529    let day = rem + 1;
3530    (year, month, day, h, mi, s)
3531}