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