Skip to main content

wifi_densepose_cli/
mat.rs

1//! MAT (Mass Casualty Assessment Tool) CLI Subcommands
2//!
3//! This module provides CLI commands for disaster response operations including:
4//! - Survivor scanning and detection
5//! - Triage status management
6//! - Alert handling
7//! - Zone configuration
8//! - Data export
9
10use anyhow::{Context, Result};
11use chrono::{DateTime, Utc};
12use clap::{Args, Subcommand, ValueEnum};
13use colored::Colorize;
14use serde::{Deserialize, Serialize};
15use std::path::PathBuf;
16use tabled::{settings::Style, Table, Tabled};
17
18use wifi_densepose_mat::{
19    DisasterConfig, DisasterType, Priority, ScanZone, TriageStatus, ZoneBounds,
20    ZoneStatus, domain::alert::AlertStatus,
21};
22
23/// MAT subcommand
24#[derive(Subcommand, Debug)]
25pub enum MatCommand {
26    /// Start scanning for survivors in disaster zones
27    Scan(ScanArgs),
28
29    /// Show current scan status
30    Status(StatusArgs),
31
32    /// Manage scan zones
33    Zones(ZonesArgs),
34
35    /// List detected survivors with triage status
36    Survivors(SurvivorsArgs),
37
38    /// View and manage alerts
39    Alerts(AlertsArgs),
40
41    /// Export scan data to JSON or CSV
42    Export(ExportArgs),
43}
44
45/// Arguments for the scan command
46#[derive(Args, Debug)]
47pub struct ScanArgs {
48    /// Zone name or ID to scan (scans all active zones if not specified)
49    #[arg(short, long)]
50    pub zone: Option<String>,
51
52    /// Disaster type for optimized detection
53    #[arg(short, long, value_enum, default_value = "earthquake")]
54    pub disaster_type: DisasterTypeArg,
55
56    /// Detection sensitivity (0.0-1.0)
57    #[arg(short, long, default_value = "0.8")]
58    pub sensitivity: f64,
59
60    /// Maximum scan depth in meters
61    #[arg(short = 'd', long, default_value = "5.0")]
62    pub max_depth: f64,
63
64    /// Enable continuous monitoring
65    #[arg(short, long)]
66    pub continuous: bool,
67
68    /// Scan interval in milliseconds (for continuous mode)
69    #[arg(short, long, default_value = "500")]
70    pub interval: u64,
71
72    /// Run in simulation mode (for testing)
73    #[arg(long)]
74    pub simulate: bool,
75}
76
77/// Disaster type argument enum for CLI
78#[derive(ValueEnum, Clone, Debug)]
79pub enum DisasterTypeArg {
80    Earthquake,
81    BuildingCollapse,
82    Avalanche,
83    Flood,
84    MineCollapse,
85    Unknown,
86}
87
88impl From<DisasterTypeArg> for DisasterType {
89    fn from(val: DisasterTypeArg) -> Self {
90        match val {
91            DisasterTypeArg::Earthquake => DisasterType::Earthquake,
92            DisasterTypeArg::BuildingCollapse => DisasterType::BuildingCollapse,
93            DisasterTypeArg::Avalanche => DisasterType::Avalanche,
94            DisasterTypeArg::Flood => DisasterType::Flood,
95            DisasterTypeArg::MineCollapse => DisasterType::MineCollapse,
96            DisasterTypeArg::Unknown => DisasterType::Unknown,
97        }
98    }
99}
100
101/// Arguments for the status command
102#[derive(Args, Debug)]
103pub struct StatusArgs {
104    /// Show detailed status including all zones
105    #[arg(short, long)]
106    pub verbose: bool,
107
108    /// Output format
109    #[arg(short, long, value_enum, default_value = "table")]
110    pub format: OutputFormat,
111
112    /// Watch mode - continuously update status
113    #[arg(short, long)]
114    pub watch: bool,
115}
116
117/// Arguments for the zones command
118#[derive(Args, Debug)]
119pub struct ZonesArgs {
120    /// Zones subcommand
121    #[command(subcommand)]
122    pub command: ZonesCommand,
123}
124
125/// Zone management subcommands
126#[derive(Subcommand, Debug)]
127pub enum ZonesCommand {
128    /// List all scan zones
129    List {
130        /// Show only active zones
131        #[arg(short, long)]
132        active: bool,
133    },
134
135    /// Add a new scan zone
136    Add {
137        /// Zone name
138        #[arg(short, long)]
139        name: String,
140
141        /// Zone type (rectangle or circle)
142        #[arg(short = 't', long, value_enum, default_value = "rectangle")]
143        zone_type: ZoneType,
144
145        /// Bounds: min_x,min_y,max_x,max_y for rectangle; center_x,center_y,radius for circle
146        #[arg(short, long)]
147        bounds: String,
148
149        /// Detection sensitivity override
150        #[arg(short, long)]
151        sensitivity: Option<f64>,
152    },
153
154    /// Remove a scan zone
155    Remove {
156        /// Zone ID or name
157        zone: String,
158
159        /// Force removal without confirmation
160        #[arg(short, long)]
161        force: bool,
162    },
163
164    /// Pause a scan zone
165    Pause {
166        /// Zone ID or name
167        zone: String,
168    },
169
170    /// Resume a paused scan zone
171    Resume {
172        /// Zone ID or name
173        zone: String,
174    },
175}
176
177/// Zone type for CLI
178#[derive(ValueEnum, Clone, Debug)]
179pub enum ZoneType {
180    Rectangle,
181    Circle,
182}
183
184/// Arguments for the survivors command
185#[derive(Args, Debug)]
186pub struct SurvivorsArgs {
187    /// Filter by triage status
188    #[arg(short, long, value_enum)]
189    pub triage: Option<TriageFilter>,
190
191    /// Filter by zone
192    #[arg(short, long)]
193    pub zone: Option<String>,
194
195    /// Sort order
196    #[arg(short, long, value_enum, default_value = "triage")]
197    pub sort_by: SortOrder,
198
199    /// Output format
200    #[arg(short, long, value_enum, default_value = "table")]
201    pub format: OutputFormat,
202
203    /// Show only active survivors
204    #[arg(short, long)]
205    pub active: bool,
206
207    /// Maximum number of results
208    #[arg(short = 'n', long)]
209    pub limit: Option<usize>,
210}
211
212/// Triage status filter for CLI
213#[derive(ValueEnum, Clone, Debug)]
214pub enum TriageFilter {
215    Immediate,
216    Delayed,
217    Minor,
218    Deceased,
219    Unknown,
220}
221
222impl From<TriageFilter> for TriageStatus {
223    fn from(val: TriageFilter) -> Self {
224        match val {
225            TriageFilter::Immediate => TriageStatus::Immediate,
226            TriageFilter::Delayed => TriageStatus::Delayed,
227            TriageFilter::Minor => TriageStatus::Minor,
228            TriageFilter::Deceased => TriageStatus::Deceased,
229            TriageFilter::Unknown => TriageStatus::Unknown,
230        }
231    }
232}
233
234/// Sort order for survivors list
235#[derive(ValueEnum, Clone, Debug)]
236pub enum SortOrder {
237    /// Sort by triage priority (most critical first)
238    Triage,
239    /// Sort by detection time (newest first)
240    Time,
241    /// Sort by zone
242    Zone,
243    /// Sort by confidence score
244    Confidence,
245}
246
247/// Output format
248#[derive(ValueEnum, Clone, Debug, Default)]
249pub enum OutputFormat {
250    /// Pretty table output
251    #[default]
252    Table,
253    /// JSON output
254    Json,
255    /// Compact single-line output
256    Compact,
257}
258
259/// Arguments for the alerts command
260#[derive(Args, Debug)]
261pub struct AlertsArgs {
262    /// Alerts subcommand
263    #[command(subcommand)]
264    pub command: Option<AlertsCommand>,
265
266    /// Filter by priority
267    #[arg(short, long, value_enum)]
268    pub priority: Option<PriorityFilter>,
269
270    /// Show only pending alerts
271    #[arg(long)]
272    pub pending: bool,
273
274    /// Maximum number of alerts to show
275    #[arg(short = 'n', long)]
276    pub limit: Option<usize>,
277}
278
279/// Alert management subcommands
280#[derive(Subcommand, Debug)]
281pub enum AlertsCommand {
282    /// List all alerts
283    List,
284
285    /// Acknowledge an alert
286    Ack {
287        /// Alert ID
288        alert_id: String,
289
290        /// Acknowledging team or person
291        #[arg(short, long)]
292        by: String,
293    },
294
295    /// Resolve an alert
296    Resolve {
297        /// Alert ID
298        alert_id: String,
299
300        /// Resolution type
301        #[arg(short, long, value_enum)]
302        resolution: ResolutionType,
303
304        /// Resolution notes
305        #[arg(short, long)]
306        notes: Option<String>,
307    },
308
309    /// Escalate an alert priority
310    Escalate {
311        /// Alert ID
312        alert_id: String,
313    },
314}
315
316/// Priority filter for CLI
317#[derive(ValueEnum, Clone, Debug)]
318pub enum PriorityFilter {
319    Critical,
320    High,
321    Medium,
322    Low,
323}
324
325/// Resolution type for CLI
326#[derive(ValueEnum, Clone, Debug)]
327pub enum ResolutionType {
328    Rescued,
329    FalsePositive,
330    Deceased,
331    Other,
332}
333
334/// Arguments for the export command
335#[derive(Args, Debug)]
336pub struct ExportArgs {
337    /// Output file path
338    #[arg(short, long)]
339    pub output: PathBuf,
340
341    /// Export format
342    #[arg(short, long, value_enum, default_value = "json")]
343    pub format: ExportFormat,
344
345    /// Include full history
346    #[arg(long)]
347    pub include_history: bool,
348
349    /// Export only survivors matching triage status
350    #[arg(short, long, value_enum)]
351    pub triage: Option<TriageFilter>,
352
353    /// Export data from specific zone
354    #[arg(short = 'z', long)]
355    pub zone: Option<String>,
356}
357
358/// Export format
359#[derive(ValueEnum, Clone, Debug)]
360pub enum ExportFormat {
361    Json,
362    Csv,
363}
364
365// ============================================================================
366// Display Structs for Tables
367// ============================================================================
368
369/// Survivor display row for tables
370#[derive(Tabled, Serialize, Deserialize)]
371struct SurvivorRow {
372    #[tabled(rename = "ID")]
373    id: String,
374    #[tabled(rename = "Zone")]
375    zone: String,
376    #[tabled(rename = "Triage")]
377    triage: String,
378    #[tabled(rename = "Status")]
379    status: String,
380    #[tabled(rename = "Confidence")]
381    confidence: String,
382    #[tabled(rename = "Location")]
383    location: String,
384    #[tabled(rename = "Last Update")]
385    last_update: String,
386}
387
388/// Zone display row for tables
389#[derive(Tabled, Serialize, Deserialize)]
390struct ZoneRow {
391    #[tabled(rename = "ID")]
392    id: String,
393    #[tabled(rename = "Name")]
394    name: String,
395    #[tabled(rename = "Status")]
396    status: String,
397    #[tabled(rename = "Area (m2)")]
398    area: String,
399    #[tabled(rename = "Scans")]
400    scan_count: u32,
401    #[tabled(rename = "Detections")]
402    detections: u32,
403    #[tabled(rename = "Last Scan")]
404    last_scan: String,
405}
406
407/// Alert display row for tables
408#[derive(Tabled, Serialize, Deserialize)]
409struct AlertRow {
410    #[tabled(rename = "ID")]
411    id: String,
412    #[tabled(rename = "Priority")]
413    priority: String,
414    #[tabled(rename = "Status")]
415    status: String,
416    #[tabled(rename = "Survivor")]
417    survivor_id: String,
418    #[tabled(rename = "Title")]
419    title: String,
420    #[tabled(rename = "Age")]
421    age: String,
422}
423
424/// Status display for system overview
425#[derive(Serialize, Deserialize)]
426struct SystemStatus {
427    scanning: bool,
428    active_zones: usize,
429    total_zones: usize,
430    survivors_detected: usize,
431    critical_survivors: usize,
432    pending_alerts: usize,
433    disaster_type: String,
434    uptime: String,
435}
436
437// ============================================================================
438// Command Execution
439// ============================================================================
440
441/// Execute a MAT command
442pub async fn execute(command: MatCommand) -> Result<()> {
443    match command {
444        MatCommand::Scan(args) => execute_scan(args).await,
445        MatCommand::Status(args) => execute_status(args).await,
446        MatCommand::Zones(args) => execute_zones(args).await,
447        MatCommand::Survivors(args) => execute_survivors(args).await,
448        MatCommand::Alerts(args) => execute_alerts(args).await,
449        MatCommand::Export(args) => execute_export(args).await,
450    }
451}
452
453/// Execute the scan command
454async fn execute_scan(args: ScanArgs) -> Result<()> {
455    println!(
456        "{} Starting survivor scan...",
457        "[MAT]".bright_cyan().bold()
458    );
459    println!();
460
461    // Display configuration
462    println!("{}", "Configuration:".bold());
463    println!(
464        "  {} {:?}",
465        "Disaster Type:".dimmed(),
466        args.disaster_type
467    );
468    println!(
469        "  {} {:.1}",
470        "Sensitivity:".dimmed(),
471        args.sensitivity
472    );
473    println!(
474        "  {} {:.1}m",
475        "Max Depth:".dimmed(),
476        args.max_depth
477    );
478    println!(
479        "  {} {}",
480        "Continuous:".dimmed(),
481        if args.continuous { "Yes" } else { "No" }
482    );
483    if args.continuous {
484        println!(
485            "  {} {}ms",
486            "Interval:".dimmed(),
487            args.interval
488        );
489    }
490    if let Some(ref zone) = args.zone {
491        println!("  {} {}", "Zone:".dimmed(), zone);
492    }
493    println!();
494
495    if args.simulate {
496        println!(
497            "{} Running in simulation mode",
498            "[SIMULATION]".yellow().bold()
499        );
500        println!();
501
502        // Simulate some detections
503        simulate_scan_output().await?;
504    } else {
505        // Build configuration
506        let config = DisasterConfig::builder()
507            .disaster_type(args.disaster_type.into())
508            .sensitivity(args.sensitivity)
509            .max_depth(args.max_depth)
510            .continuous_monitoring(args.continuous)
511            .scan_interval_ms(args.interval)
512            .build();
513
514        println!(
515            "{} Initializing detection pipeline with config: {:?}",
516            "[INFO]".blue(),
517            config.disaster_type
518        );
519        println!(
520            "{} Waiting for hardware connection...",
521            "[INFO]".blue()
522        );
523        println!();
524        println!(
525            "{} No hardware detected. Use --simulate for demo mode.",
526            "[WARN]".yellow()
527        );
528    }
529
530    Ok(())
531}
532
533/// Simulate scan output for demonstration
534async fn simulate_scan_output() -> Result<()> {
535    use indicatif::{ProgressBar, ProgressStyle};
536    use std::time::Duration;
537
538    let pb = ProgressBar::new(100);
539    pb.set_style(
540        ProgressStyle::default_bar()
541            .template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta})")?
542            .progress_chars("#>-"),
543    );
544
545    for i in 0..100 {
546        pb.set_position(i);
547        tokio::time::sleep(Duration::from_millis(50)).await;
548
549        // Simulate detection events
550        if i == 25 {
551            pb.suspend(|| {
552                println!();
553                print_detection(
554                    "SURV-001",
555                    "Zone A",
556                    TriageStatus::Immediate,
557                    0.92,
558                    Some((12.5, 8.3, -2.1)),
559                );
560            });
561        }
562        if i == 55 {
563            pb.suspend(|| {
564                print_detection(
565                    "SURV-002",
566                    "Zone A",
567                    TriageStatus::Delayed,
568                    0.78,
569                    Some((15.2, 10.1, -1.5)),
570                );
571            });
572        }
573        if i == 80 {
574            pb.suspend(|| {
575                print_detection(
576                    "SURV-003",
577                    "Zone B",
578                    TriageStatus::Minor,
579                    0.85,
580                    Some((8.7, 22.4, -0.8)),
581                );
582            });
583        }
584    }
585
586    pb.finish_with_message("Scan complete");
587    println!();
588    println!(
589        "{} Scan complete. Detected {} survivors.",
590        "[MAT]".bright_cyan().bold(),
591        "3".green().bold()
592    );
593    println!(
594        "  {} {}  {} {}  {} {}",
595        "IMMEDIATE:".red().bold(),
596        "1",
597        "DELAYED:".yellow().bold(),
598        "1",
599        "MINOR:".green().bold(),
600        "1"
601    );
602
603    Ok(())
604}
605
606/// Print a detection event
607fn print_detection(
608    id: &str,
609    zone: &str,
610    triage: TriageStatus,
611    confidence: f64,
612    location: Option<(f64, f64, f64)>,
613) {
614    let triage_str = format_triage(&triage);
615    let location_str = location
616        .map(|(x, y, z)| format!("({:.1}, {:.1}, {:.1})", x, y, z))
617        .unwrap_or_else(|| "Unknown".to_string());
618
619    println!(
620        "{} {} detected in {} - {} | Confidence: {:.0}% | Location: {}",
621        format!("[{}]", triage_str).bold(),
622        id.cyan(),
623        zone,
624        triage_str,
625        confidence * 100.0,
626        location_str.dimmed()
627    );
628}
629
630/// Execute the status command
631async fn execute_status(args: StatusArgs) -> Result<()> {
632    // In a real implementation, this would connect to a running daemon
633    let status = SystemStatus {
634        scanning: false,
635        active_zones: 0,
636        total_zones: 0,
637        survivors_detected: 0,
638        critical_survivors: 0,
639        pending_alerts: 0,
640        disaster_type: "Not configured".to_string(),
641        uptime: "N/A".to_string(),
642    };
643
644    match args.format {
645        OutputFormat::Json => {
646            println!("{}", serde_json::to_string_pretty(&status)?);
647        }
648        OutputFormat::Compact => {
649            println!(
650                "scanning={} zones={}/{} survivors={} critical={} alerts={}",
651                status.scanning,
652                status.active_zones,
653                status.total_zones,
654                status.survivors_detected,
655                status.critical_survivors,
656                status.pending_alerts
657            );
658        }
659        OutputFormat::Table => {
660            println!("{}", "MAT System Status".bold().cyan());
661            println!("{}", "=".repeat(50));
662            println!(
663                "  {} {}",
664                "Scanning:".dimmed(),
665                if status.scanning {
666                    "Active".green()
667                } else {
668                    "Inactive".red()
669                }
670            );
671            println!(
672                "  {} {}/{}",
673                "Zones:".dimmed(),
674                status.active_zones,
675                status.total_zones
676            );
677            println!(
678                "  {} {}",
679                "Disaster Type:".dimmed(),
680                status.disaster_type
681            );
682            println!(
683                "  {} {}",
684                "Survivors Detected:".dimmed(),
685                status.survivors_detected
686            );
687            println!(
688                "  {} {}",
689                "Critical (Immediate):".dimmed(),
690                if status.critical_survivors > 0 {
691                    status.critical_survivors.to_string().red().bold()
692                } else {
693                    status.critical_survivors.to_string().normal()
694                }
695            );
696            println!(
697                "  {} {}",
698                "Pending Alerts:".dimmed(),
699                if status.pending_alerts > 0 {
700                    status.pending_alerts.to_string().yellow().bold()
701                } else {
702                    status.pending_alerts.to_string().normal()
703                }
704            );
705            println!("  {} {}", "Uptime:".dimmed(), status.uptime);
706            println!();
707
708            if !status.scanning {
709                println!(
710                    "{} No active scan. Run '{}' to start.",
711                    "[INFO]".blue(),
712                    "wifi-densepose mat scan".green()
713                );
714            }
715        }
716    }
717
718    Ok(())
719}
720
721/// Execute the zones command
722async fn execute_zones(args: ZonesArgs) -> Result<()> {
723    match args.command {
724        ZonesCommand::List { active } => {
725            println!("{}", "Scan Zones".bold().cyan());
726            println!("{}", "=".repeat(80));
727
728            // Demo data
729            let zones = vec![
730                ZoneRow {
731                    id: "zone-001".to_string(),
732                    name: "Building A - North Wing".to_string(),
733                    status: format_zone_status(&ZoneStatus::Active),
734                    area: "1500.0".to_string(),
735                    scan_count: 42,
736                    detections: 3,
737                    last_scan: "2 min ago".to_string(),
738                },
739                ZoneRow {
740                    id: "zone-002".to_string(),
741                    name: "Building A - South Wing".to_string(),
742                    status: format_zone_status(&ZoneStatus::Paused),
743                    area: "1200.0".to_string(),
744                    scan_count: 28,
745                    detections: 1,
746                    last_scan: "15 min ago".to_string(),
747                },
748            ];
749
750            let filtered: Vec<_> = if active {
751                zones
752                    .into_iter()
753                    .filter(|z| z.status.contains("Active"))
754                    .collect()
755            } else {
756                zones
757            };
758
759            if filtered.is_empty() {
760                println!("No zones configured. Use 'wifi-densepose mat zones add' to create one.");
761            } else {
762                let table = Table::new(filtered).with(Style::rounded()).to_string();
763                println!("{}", table);
764            }
765        }
766        ZonesCommand::Add {
767            name,
768            zone_type,
769            bounds,
770            sensitivity,
771        } => {
772            // Parse bounds
773            let bounds_parsed: Result<ZoneBounds, _> = parse_bounds(&zone_type, &bounds);
774            match bounds_parsed {
775                Ok(zone_bounds) => {
776                    let zone = if let Some(sens) = sensitivity {
777                        let mut params = wifi_densepose_mat::ScanParameters::default();
778                        params.sensitivity = sens;
779                        ScanZone::with_parameters(&name, zone_bounds, params)
780                    } else {
781                        ScanZone::new(&name, zone_bounds)
782                    };
783
784                    println!(
785                        "{} Zone '{}' created with ID: {}",
786                        "[OK]".green().bold(),
787                        name.cyan(),
788                        zone.id()
789                    );
790                    println!("  Area: {:.1} m2", zone.area());
791                }
792                Err(e) => {
793                    eprintln!("{} Failed to parse bounds: {}", "[ERROR]".red().bold(), e);
794                    eprintln!("  Expected format for rectangle: min_x,min_y,max_x,max_y");
795                    eprintln!("  Expected format for circle: center_x,center_y,radius");
796                    return Err(e);
797                }
798            }
799        }
800        ZonesCommand::Remove { zone, force } => {
801            if !force {
802                println!(
803                    "{} This will remove zone '{}' and stop any active scans.",
804                    "[WARN]".yellow().bold(),
805                    zone
806                );
807                println!("Use --force to confirm.");
808            } else {
809                println!(
810                    "{} Zone '{}' removed.",
811                    "[OK]".green().bold(),
812                    zone.cyan()
813                );
814            }
815        }
816        ZonesCommand::Pause { zone } => {
817            println!(
818                "{} Zone '{}' paused.",
819                "[OK]".green().bold(),
820                zone.cyan()
821            );
822        }
823        ZonesCommand::Resume { zone } => {
824            println!(
825                "{} Zone '{}' resumed.",
826                "[OK]".green().bold(),
827                zone.cyan()
828            );
829        }
830    }
831
832    Ok(())
833}
834
835/// Parse bounds string into ZoneBounds
836fn parse_bounds(zone_type: &ZoneType, bounds: &str) -> Result<ZoneBounds> {
837    let parts: Vec<f64> = bounds
838        .split(',')
839        .map(|s| s.trim().parse::<f64>())
840        .collect::<std::result::Result<Vec<_>, _>>()
841        .context("Failed to parse bounds values as numbers")?;
842
843    match zone_type {
844        ZoneType::Rectangle => {
845            if parts.len() != 4 {
846                anyhow::bail!(
847                    "Rectangle requires 4 values: min_x,min_y,max_x,max_y (got {})",
848                    parts.len()
849                );
850            }
851            Ok(ZoneBounds::rectangle(parts[0], parts[1], parts[2], parts[3]))
852        }
853        ZoneType::Circle => {
854            if parts.len() != 3 {
855                anyhow::bail!(
856                    "Circle requires 3 values: center_x,center_y,radius (got {})",
857                    parts.len()
858                );
859            }
860            Ok(ZoneBounds::circle(parts[0], parts[1], parts[2]))
861        }
862    }
863}
864
865/// Execute the survivors command
866async fn execute_survivors(args: SurvivorsArgs) -> Result<()> {
867    // Demo data
868    let survivors = vec![
869        SurvivorRow {
870            id: "SURV-001".to_string(),
871            zone: "Zone A".to_string(),
872            triage: format_triage(&TriageStatus::Immediate),
873            status: "Active".green().to_string(),
874            confidence: "92%".to_string(),
875            location: "(12.5, 8.3, -2.1)".to_string(),
876            last_update: "30s ago".to_string(),
877        },
878        SurvivorRow {
879            id: "SURV-002".to_string(),
880            zone: "Zone A".to_string(),
881            triage: format_triage(&TriageStatus::Delayed),
882            status: "Active".green().to_string(),
883            confidence: "78%".to_string(),
884            location: "(15.2, 10.1, -1.5)".to_string(),
885            last_update: "1m ago".to_string(),
886        },
887        SurvivorRow {
888            id: "SURV-003".to_string(),
889            zone: "Zone B".to_string(),
890            triage: format_triage(&TriageStatus::Minor),
891            status: "Active".green().to_string(),
892            confidence: "85%".to_string(),
893            location: "(8.7, 22.4, -0.8)".to_string(),
894            last_update: "2m ago".to_string(),
895        },
896    ];
897
898    // Apply filters
899    let mut filtered = survivors;
900
901    if let Some(ref triage_filter) = args.triage {
902        let status: TriageStatus = triage_filter.clone().into();
903        let status_str = format_triage(&status);
904        filtered.retain(|s| s.triage == status_str);
905    }
906
907    if let Some(ref zone) = args.zone {
908        filtered.retain(|s| s.zone.contains(zone));
909    }
910
911    if let Some(limit) = args.limit {
912        filtered.truncate(limit);
913    }
914
915    match args.format {
916        OutputFormat::Json => {
917            println!("{}", serde_json::to_string_pretty(&filtered)?);
918        }
919        OutputFormat::Compact => {
920            for s in &filtered {
921                println!(
922                    "{}\t{}\t{}\t{}\t{}",
923                    s.id, s.zone, s.triage, s.confidence, s.location
924                );
925            }
926        }
927        OutputFormat::Table => {
928            println!("{}", "Detected Survivors".bold().cyan());
929            println!("{}", "=".repeat(100));
930
931            if filtered.is_empty() {
932                println!("No survivors detected matching criteria.");
933            } else {
934                // Print summary
935                let immediate = filtered
936                    .iter()
937                    .filter(|s| s.triage.contains("IMMEDIATE"))
938                    .count();
939                let delayed = filtered
940                    .iter()
941                    .filter(|s| s.triage.contains("DELAYED"))
942                    .count();
943                let minor = filtered
944                    .iter()
945                    .filter(|s| s.triage.contains("MINOR"))
946                    .count();
947
948                println!(
949                    "Total: {} | {} {} | {} {} | {} {}",
950                    filtered.len().to_string().bold(),
951                    "IMMEDIATE:".red().bold(),
952                    immediate,
953                    "DELAYED:".yellow().bold(),
954                    delayed,
955                    "MINOR:".green().bold(),
956                    minor
957                );
958                println!();
959
960                let table = Table::new(filtered).with(Style::rounded()).to_string();
961                println!("{}", table);
962            }
963        }
964    }
965
966    Ok(())
967}
968
969/// Execute the alerts command
970async fn execute_alerts(args: AlertsArgs) -> Result<()> {
971    match args.command {
972        Some(AlertsCommand::Ack { alert_id, by }) => {
973            println!(
974                "{} Alert {} acknowledged by {}",
975                "[OK]".green().bold(),
976                alert_id.cyan(),
977                by
978            );
979        }
980        Some(AlertsCommand::Resolve {
981            alert_id,
982            resolution,
983            notes,
984        }) => {
985            println!(
986                "{} Alert {} resolved as {:?}",
987                "[OK]".green().bold(),
988                alert_id.cyan(),
989                resolution
990            );
991            if let Some(notes) = notes {
992                println!("  Notes: {}", notes);
993            }
994        }
995        Some(AlertsCommand::Escalate { alert_id }) => {
996            println!(
997                "{} Alert {} escalated to higher priority",
998                "[OK]".green().bold(),
999                alert_id.cyan()
1000            );
1001        }
1002        Some(AlertsCommand::List) | None => {
1003            // Demo data
1004            let alerts = vec![
1005                AlertRow {
1006                    id: "ALRT-001".to_string(),
1007                    priority: format_priority(Priority::Critical),
1008                    status: format_alert_status(&AlertStatus::Pending),
1009                    survivor_id: "SURV-001".to_string(),
1010                    title: "Immediate: Survivor detected".to_string(),
1011                    age: "5m".to_string(),
1012                },
1013                AlertRow {
1014                    id: "ALRT-002".to_string(),
1015                    priority: format_priority(Priority::High),
1016                    status: format_alert_status(&AlertStatus::Acknowledged),
1017                    survivor_id: "SURV-002".to_string(),
1018                    title: "Delayed: Survivor needs attention".to_string(),
1019                    age: "12m".to_string(),
1020                },
1021            ];
1022
1023            let mut filtered = alerts;
1024
1025            if args.pending {
1026                filtered.retain(|a| a.status.contains("Pending"));
1027            }
1028
1029            if let Some(limit) = args.limit {
1030                filtered.truncate(limit);
1031            }
1032
1033            println!("{}", "Alerts".bold().cyan());
1034            println!("{}", "=".repeat(100));
1035
1036            if filtered.is_empty() {
1037                println!("No alerts.");
1038            } else {
1039                let pending = filtered.iter().filter(|a| a.status.contains("Pending")).count();
1040                if pending > 0 {
1041                    println!(
1042                        "{} {} pending alert(s) require attention!",
1043                        "[ALERT]".red().bold(),
1044                        pending
1045                    );
1046                    println!();
1047                }
1048
1049                let table = Table::new(filtered).with(Style::rounded()).to_string();
1050                println!("{}", table);
1051            }
1052        }
1053    }
1054
1055    Ok(())
1056}
1057
1058/// Execute the export command
1059async fn execute_export(args: ExportArgs) -> Result<()> {
1060    println!(
1061        "{} Exporting data to {}...",
1062        "[INFO]".blue(),
1063        args.output.display()
1064    );
1065
1066    // Demo export data
1067    #[derive(Serialize)]
1068    struct ExportData {
1069        exported_at: DateTime<Utc>,
1070        survivors: Vec<SurvivorExport>,
1071        zones: Vec<ZoneExport>,
1072        alerts: Vec<AlertExport>,
1073    }
1074
1075    #[derive(Serialize)]
1076    struct SurvivorExport {
1077        id: String,
1078        zone_id: String,
1079        triage_status: String,
1080        confidence: f64,
1081        location: Option<(f64, f64, f64)>,
1082        first_detected: DateTime<Utc>,
1083        last_updated: DateTime<Utc>,
1084    }
1085
1086    #[derive(Serialize)]
1087    struct ZoneExport {
1088        id: String,
1089        name: String,
1090        status: String,
1091        area: f64,
1092        scan_count: u32,
1093    }
1094
1095    #[derive(Serialize)]
1096    struct AlertExport {
1097        id: String,
1098        priority: String,
1099        status: String,
1100        survivor_id: String,
1101        created_at: DateTime<Utc>,
1102    }
1103
1104    let data = ExportData {
1105        exported_at: Utc::now(),
1106        survivors: vec![SurvivorExport {
1107            id: "SURV-001".to_string(),
1108            zone_id: "zone-001".to_string(),
1109            triage_status: "Immediate".to_string(),
1110            confidence: 0.92,
1111            location: Some((12.5, 8.3, -2.1)),
1112            first_detected: Utc::now() - chrono::Duration::minutes(15),
1113            last_updated: Utc::now() - chrono::Duration::seconds(30),
1114        }],
1115        zones: vec![ZoneExport {
1116            id: "zone-001".to_string(),
1117            name: "Building A - North Wing".to_string(),
1118            status: "Active".to_string(),
1119            area: 1500.0,
1120            scan_count: 42,
1121        }],
1122        alerts: vec![AlertExport {
1123            id: "ALRT-001".to_string(),
1124            priority: "Critical".to_string(),
1125            status: "Pending".to_string(),
1126            survivor_id: "SURV-001".to_string(),
1127            created_at: Utc::now() - chrono::Duration::minutes(5),
1128        }],
1129    };
1130
1131    match args.format {
1132        ExportFormat::Json => {
1133            let json = serde_json::to_string_pretty(&data)?;
1134            std::fs::write(&args.output, json)?;
1135        }
1136        ExportFormat::Csv => {
1137            let mut wtr = csv::Writer::from_path(&args.output)?;
1138            for survivor in &data.survivors {
1139                wtr.serialize(survivor)?;
1140            }
1141            wtr.flush()?;
1142        }
1143    }
1144
1145    println!(
1146        "{} Export complete: {}",
1147        "[OK]".green().bold(),
1148        args.output.display()
1149    );
1150
1151    Ok(())
1152}
1153
1154// ============================================================================
1155// Formatting Helpers
1156// ============================================================================
1157
1158/// Format triage status with color
1159fn format_triage(status: &TriageStatus) -> String {
1160    match status {
1161        TriageStatus::Immediate => "IMMEDIATE (Red)".red().bold().to_string(),
1162        TriageStatus::Delayed => "DELAYED (Yellow)".yellow().bold().to_string(),
1163        TriageStatus::Minor => "MINOR (Green)".green().bold().to_string(),
1164        TriageStatus::Deceased => "DECEASED (Black)".dimmed().to_string(),
1165        TriageStatus::Unknown => "UNKNOWN".dimmed().to_string(),
1166    }
1167}
1168
1169/// Format zone status with color
1170fn format_zone_status(status: &ZoneStatus) -> String {
1171    match status {
1172        ZoneStatus::Active => "Active".green().to_string(),
1173        ZoneStatus::Paused => "Paused".yellow().to_string(),
1174        ZoneStatus::Complete => "Complete".blue().to_string(),
1175        ZoneStatus::Inaccessible => "Inaccessible".red().to_string(),
1176        ZoneStatus::Deactivated => "Deactivated".dimmed().to_string(),
1177    }
1178}
1179
1180/// Format priority with color
1181fn format_priority(priority: Priority) -> String {
1182    match priority {
1183        Priority::Critical => "CRITICAL".red().bold().to_string(),
1184        Priority::High => "HIGH".bright_red().to_string(),
1185        Priority::Medium => "MEDIUM".yellow().to_string(),
1186        Priority::Low => "LOW".blue().to_string(),
1187    }
1188}
1189
1190/// Format alert status with color
1191fn format_alert_status(status: &AlertStatus) -> String {
1192    match status {
1193        AlertStatus::Pending => "Pending".red().to_string(),
1194        AlertStatus::Acknowledged => "Acknowledged".yellow().to_string(),
1195        AlertStatus::InProgress => "In Progress".blue().to_string(),
1196        AlertStatus::Resolved => "Resolved".green().to_string(),
1197        AlertStatus::Cancelled => "Cancelled".dimmed().to_string(),
1198        AlertStatus::Expired => "Expired".dimmed().to_string(),
1199    }
1200}
1201
1202#[cfg(test)]
1203mod tests {
1204    use super::*;
1205
1206    #[test]
1207    fn test_parse_rectangle_bounds() {
1208        let result = parse_bounds(&ZoneType::Rectangle, "0,0,10,20");
1209        assert!(result.is_ok());
1210    }
1211
1212    #[test]
1213    fn test_parse_circle_bounds() {
1214        let result = parse_bounds(&ZoneType::Circle, "5,5,10");
1215        assert!(result.is_ok());
1216    }
1217
1218    #[test]
1219    fn test_parse_invalid_bounds() {
1220        let result = parse_bounds(&ZoneType::Rectangle, "invalid");
1221        assert!(result.is_err());
1222    }
1223
1224    #[test]
1225    fn test_disaster_type_conversion() {
1226        let dt: DisasterType = DisasterTypeArg::Earthquake.into();
1227        assert!(matches!(dt, DisasterType::Earthquake));
1228    }
1229
1230    #[test]
1231    fn test_triage_filter_conversion() {
1232        let ts: TriageStatus = TriageFilter::Immediate.into();
1233        assert!(matches!(ts, TriageStatus::Immediate));
1234    }
1235}