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    domain::alert::AlertStatus, DisasterConfig, DisasterType, Priority, ScanZone, TriageStatus,
20    ZoneBounds, ZoneStatus,
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!("{} Starting survivor scan...", "[MAT]".bright_cyan().bold());
456    println!();
457
458    // Display configuration
459    println!("{}", "Configuration:".bold());
460    println!("  {} {:?}", "Disaster Type:".dimmed(), args.disaster_type);
461    println!("  {} {:.1}", "Sensitivity:".dimmed(), args.sensitivity);
462    println!("  {} {:.1}m", "Max Depth:".dimmed(), args.max_depth);
463    println!(
464        "  {} {}",
465        "Continuous:".dimmed(),
466        if args.continuous { "Yes" } else { "No" }
467    );
468    if args.continuous {
469        println!("  {} {}ms", "Interval:".dimmed(), args.interval);
470    }
471    if let Some(ref zone) = args.zone {
472        println!("  {} {}", "Zone:".dimmed(), zone);
473    }
474    println!();
475
476    if args.simulate {
477        println!(
478            "{} Running in simulation mode",
479            "[SIMULATION]".yellow().bold()
480        );
481        println!();
482
483        // Simulate some detections
484        simulate_scan_output().await?;
485    } else {
486        // Build configuration
487        let config = DisasterConfig::builder()
488            .disaster_type(args.disaster_type.into())
489            .sensitivity(args.sensitivity)
490            .max_depth(args.max_depth)
491            .continuous_monitoring(args.continuous)
492            .scan_interval_ms(args.interval)
493            .build();
494
495        println!(
496            "{} Initializing detection pipeline with config: {:?}",
497            "[INFO]".blue(),
498            config.disaster_type
499        );
500        println!("{} Waiting for hardware connection...", "[INFO]".blue());
501        println!();
502        println!(
503            "{} No hardware detected. Use --simulate for demo mode.",
504            "[WARN]".yellow()
505        );
506    }
507
508    Ok(())
509}
510
511/// Simulate scan output for demonstration
512async fn simulate_scan_output() -> Result<()> {
513    use indicatif::{ProgressBar, ProgressStyle};
514    use std::time::Duration;
515
516    let pb = ProgressBar::new(100);
517    pb.set_style(
518        ProgressStyle::default_bar()
519            .template(
520                "{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta})",
521            )?
522            .progress_chars("#>-"),
523    );
524
525    for i in 0..100 {
526        pb.set_position(i);
527        tokio::time::sleep(Duration::from_millis(50)).await;
528
529        // Simulate detection events
530        if i == 25 {
531            pb.suspend(|| {
532                println!();
533                print_detection(
534                    "SURV-001",
535                    "Zone A",
536                    TriageStatus::Immediate,
537                    0.92,
538                    Some((12.5, 8.3, -2.1)),
539                );
540            });
541        }
542        if i == 55 {
543            pb.suspend(|| {
544                print_detection(
545                    "SURV-002",
546                    "Zone A",
547                    TriageStatus::Delayed,
548                    0.78,
549                    Some((15.2, 10.1, -1.5)),
550                );
551            });
552        }
553        if i == 80 {
554            pb.suspend(|| {
555                print_detection(
556                    "SURV-003",
557                    "Zone B",
558                    TriageStatus::Minor,
559                    0.85,
560                    Some((8.7, 22.4, -0.8)),
561                );
562            });
563        }
564    }
565
566    pb.finish_with_message("Scan complete");
567    println!();
568    println!(
569        "{} Scan complete. Detected {} survivors.",
570        "[MAT]".bright_cyan().bold(),
571        "3".green().bold()
572    );
573    println!(
574        "  {} 1  {} 1  {} 1",
575        "IMMEDIATE:".red().bold(),
576        "DELAYED:".yellow().bold(),
577        "MINOR:".green().bold(),
578    );
579
580    Ok(())
581}
582
583/// Print a detection event
584fn print_detection(
585    id: &str,
586    zone: &str,
587    triage: TriageStatus,
588    confidence: f64,
589    location: Option<(f64, f64, f64)>,
590) {
591    let triage_str = format_triage(&triage);
592    let location_str = location
593        .map(|(x, y, z)| format!("({:.1}, {:.1}, {:.1})", x, y, z))
594        .unwrap_or_else(|| "Unknown".to_string());
595
596    println!(
597        "{} {} detected in {} - {} | Confidence: {:.0}% | Location: {}",
598        format!("[{}]", triage_str).bold(),
599        id.cyan(),
600        zone,
601        triage_str,
602        confidence * 100.0,
603        location_str.dimmed()
604    );
605}
606
607/// Execute the status command
608async fn execute_status(args: StatusArgs) -> Result<()> {
609    // In a real implementation, this would connect to a running daemon
610    let status = SystemStatus {
611        scanning: false,
612        active_zones: 0,
613        total_zones: 0,
614        survivors_detected: 0,
615        critical_survivors: 0,
616        pending_alerts: 0,
617        disaster_type: "Not configured".to_string(),
618        uptime: "N/A".to_string(),
619    };
620
621    match args.format {
622        OutputFormat::Json => {
623            println!("{}", serde_json::to_string_pretty(&status)?);
624        }
625        OutputFormat::Compact => {
626            println!(
627                "scanning={} zones={}/{} survivors={} critical={} alerts={}",
628                status.scanning,
629                status.active_zones,
630                status.total_zones,
631                status.survivors_detected,
632                status.critical_survivors,
633                status.pending_alerts
634            );
635        }
636        OutputFormat::Table => {
637            println!("{}", "MAT System Status".bold().cyan());
638            println!("{}", "=".repeat(50));
639            println!(
640                "  {} {}",
641                "Scanning:".dimmed(),
642                if status.scanning {
643                    "Active".green()
644                } else {
645                    "Inactive".red()
646                }
647            );
648            println!(
649                "  {} {}/{}",
650                "Zones:".dimmed(),
651                status.active_zones,
652                status.total_zones
653            );
654            println!("  {} {}", "Disaster Type:".dimmed(), status.disaster_type);
655            println!(
656                "  {} {}",
657                "Survivors Detected:".dimmed(),
658                status.survivors_detected
659            );
660            println!(
661                "  {} {}",
662                "Critical (Immediate):".dimmed(),
663                if status.critical_survivors > 0 {
664                    status.critical_survivors.to_string().red().bold()
665                } else {
666                    status.critical_survivors.to_string().normal()
667                }
668            );
669            println!(
670                "  {} {}",
671                "Pending Alerts:".dimmed(),
672                if status.pending_alerts > 0 {
673                    status.pending_alerts.to_string().yellow().bold()
674                } else {
675                    status.pending_alerts.to_string().normal()
676                }
677            );
678            println!("  {} {}", "Uptime:".dimmed(), status.uptime);
679            println!();
680
681            if !status.scanning {
682                println!(
683                    "{} No active scan. Run '{}' to start.",
684                    "[INFO]".blue(),
685                    "wifi-densepose mat scan".green()
686                );
687            }
688        }
689    }
690
691    Ok(())
692}
693
694/// Execute the zones command
695async fn execute_zones(args: ZonesArgs) -> Result<()> {
696    match args.command {
697        ZonesCommand::List { active } => {
698            println!("{}", "Scan Zones".bold().cyan());
699            println!("{}", "=".repeat(80));
700
701            // Demo data
702            let zones = vec![
703                ZoneRow {
704                    id: "zone-001".to_string(),
705                    name: "Building A - North Wing".to_string(),
706                    status: format_zone_status(&ZoneStatus::Active),
707                    area: "1500.0".to_string(),
708                    scan_count: 42,
709                    detections: 3,
710                    last_scan: "2 min ago".to_string(),
711                },
712                ZoneRow {
713                    id: "zone-002".to_string(),
714                    name: "Building A - South Wing".to_string(),
715                    status: format_zone_status(&ZoneStatus::Paused),
716                    area: "1200.0".to_string(),
717                    scan_count: 28,
718                    detections: 1,
719                    last_scan: "15 min ago".to_string(),
720                },
721            ];
722
723            let filtered: Vec<_> = if active {
724                zones
725                    .into_iter()
726                    .filter(|z| z.status.contains("Active"))
727                    .collect()
728            } else {
729                zones
730            };
731
732            if filtered.is_empty() {
733                println!("No zones configured. Use 'wifi-densepose mat zones add' to create one.");
734            } else {
735                let table = Table::new(filtered).with(Style::rounded()).to_string();
736                println!("{}", table);
737            }
738        }
739        ZonesCommand::Add {
740            name,
741            zone_type,
742            bounds,
743            sensitivity,
744        } => {
745            // Parse bounds
746            let bounds_parsed: Result<ZoneBounds, _> = parse_bounds(&zone_type, &bounds);
747            match bounds_parsed {
748                Ok(zone_bounds) => {
749                    let zone = if let Some(sens) = sensitivity {
750                        let params = wifi_densepose_mat::ScanParameters {
751                            sensitivity: sens,
752                            ..Default::default()
753                        };
754                        ScanZone::with_parameters(&name, zone_bounds, params)
755                    } else {
756                        ScanZone::new(&name, zone_bounds)
757                    };
758
759                    println!(
760                        "{} Zone '{}' created with ID: {}",
761                        "[OK]".green().bold(),
762                        name.cyan(),
763                        zone.id()
764                    );
765                    println!("  Area: {:.1} m2", zone.area());
766                }
767                Err(e) => {
768                    eprintln!("{} Failed to parse bounds: {}", "[ERROR]".red().bold(), e);
769                    eprintln!("  Expected format for rectangle: min_x,min_y,max_x,max_y");
770                    eprintln!("  Expected format for circle: center_x,center_y,radius");
771                    return Err(e);
772                }
773            }
774        }
775        ZonesCommand::Remove { zone, force } => {
776            if !force {
777                println!(
778                    "{} This will remove zone '{}' and stop any active scans.",
779                    "[WARN]".yellow().bold(),
780                    zone
781                );
782                println!("Use --force to confirm.");
783            } else {
784                println!("{} Zone '{}' removed.", "[OK]".green().bold(), zone.cyan());
785            }
786        }
787        ZonesCommand::Pause { zone } => {
788            println!("{} Zone '{}' paused.", "[OK]".green().bold(), zone.cyan());
789        }
790        ZonesCommand::Resume { zone } => {
791            println!("{} Zone '{}' resumed.", "[OK]".green().bold(), zone.cyan());
792        }
793    }
794
795    Ok(())
796}
797
798/// Parse bounds string into ZoneBounds
799fn parse_bounds(zone_type: &ZoneType, bounds: &str) -> Result<ZoneBounds> {
800    let parts: Vec<f64> = bounds
801        .split(',')
802        .map(|s| s.trim().parse::<f64>())
803        .collect::<std::result::Result<Vec<_>, _>>()
804        .context("Failed to parse bounds values as numbers")?;
805
806    match zone_type {
807        ZoneType::Rectangle => {
808            if parts.len() != 4 {
809                anyhow::bail!(
810                    "Rectangle requires 4 values: min_x,min_y,max_x,max_y (got {})",
811                    parts.len()
812                );
813            }
814            Ok(ZoneBounds::rectangle(
815                parts[0], parts[1], parts[2], parts[3],
816            ))
817        }
818        ZoneType::Circle => {
819            if parts.len() != 3 {
820                anyhow::bail!(
821                    "Circle requires 3 values: center_x,center_y,radius (got {})",
822                    parts.len()
823                );
824            }
825            Ok(ZoneBounds::circle(parts[0], parts[1], parts[2]))
826        }
827    }
828}
829
830/// Execute the survivors command
831async fn execute_survivors(args: SurvivorsArgs) -> Result<()> {
832    // Demo data
833    let survivors = vec![
834        SurvivorRow {
835            id: "SURV-001".to_string(),
836            zone: "Zone A".to_string(),
837            triage: format_triage(&TriageStatus::Immediate),
838            status: "Active".green().to_string(),
839            confidence: "92%".to_string(),
840            location: "(12.5, 8.3, -2.1)".to_string(),
841            last_update: "30s ago".to_string(),
842        },
843        SurvivorRow {
844            id: "SURV-002".to_string(),
845            zone: "Zone A".to_string(),
846            triage: format_triage(&TriageStatus::Delayed),
847            status: "Active".green().to_string(),
848            confidence: "78%".to_string(),
849            location: "(15.2, 10.1, -1.5)".to_string(),
850            last_update: "1m ago".to_string(),
851        },
852        SurvivorRow {
853            id: "SURV-003".to_string(),
854            zone: "Zone B".to_string(),
855            triage: format_triage(&TriageStatus::Minor),
856            status: "Active".green().to_string(),
857            confidence: "85%".to_string(),
858            location: "(8.7, 22.4, -0.8)".to_string(),
859            last_update: "2m ago".to_string(),
860        },
861    ];
862
863    // Apply filters
864    let mut filtered = survivors;
865
866    if let Some(ref triage_filter) = args.triage {
867        let status: TriageStatus = triage_filter.clone().into();
868        let status_str = format_triage(&status);
869        filtered.retain(|s| s.triage == status_str);
870    }
871
872    if let Some(ref zone) = args.zone {
873        filtered.retain(|s| s.zone.contains(zone));
874    }
875
876    if let Some(limit) = args.limit {
877        filtered.truncate(limit);
878    }
879
880    match args.format {
881        OutputFormat::Json => {
882            println!("{}", serde_json::to_string_pretty(&filtered)?);
883        }
884        OutputFormat::Compact => {
885            for s in &filtered {
886                println!(
887                    "{}\t{}\t{}\t{}\t{}",
888                    s.id, s.zone, s.triage, s.confidence, s.location
889                );
890            }
891        }
892        OutputFormat::Table => {
893            println!("{}", "Detected Survivors".bold().cyan());
894            println!("{}", "=".repeat(100));
895
896            if filtered.is_empty() {
897                println!("No survivors detected matching criteria.");
898            } else {
899                // Print summary
900                let immediate = filtered
901                    .iter()
902                    .filter(|s| s.triage.contains("IMMEDIATE"))
903                    .count();
904                let delayed = filtered
905                    .iter()
906                    .filter(|s| s.triage.contains("DELAYED"))
907                    .count();
908                let minor = filtered
909                    .iter()
910                    .filter(|s| s.triage.contains("MINOR"))
911                    .count();
912
913                println!(
914                    "Total: {} | {} {} | {} {} | {} {}",
915                    filtered.len().to_string().bold(),
916                    "IMMEDIATE:".red().bold(),
917                    immediate,
918                    "DELAYED:".yellow().bold(),
919                    delayed,
920                    "MINOR:".green().bold(),
921                    minor
922                );
923                println!();
924
925                let table = Table::new(filtered).with(Style::rounded()).to_string();
926                println!("{}", table);
927            }
928        }
929    }
930
931    Ok(())
932}
933
934/// Execute the alerts command
935async fn execute_alerts(args: AlertsArgs) -> Result<()> {
936    match args.command {
937        Some(AlertsCommand::Ack { alert_id, by }) => {
938            println!(
939                "{} Alert {} acknowledged by {}",
940                "[OK]".green().bold(),
941                alert_id.cyan(),
942                by
943            );
944        }
945        Some(AlertsCommand::Resolve {
946            alert_id,
947            resolution,
948            notes,
949        }) => {
950            println!(
951                "{} Alert {} resolved as {:?}",
952                "[OK]".green().bold(),
953                alert_id.cyan(),
954                resolution
955            );
956            if let Some(notes) = notes {
957                println!("  Notes: {}", notes);
958            }
959        }
960        Some(AlertsCommand::Escalate { alert_id }) => {
961            println!(
962                "{} Alert {} escalated to higher priority",
963                "[OK]".green().bold(),
964                alert_id.cyan()
965            );
966        }
967        Some(AlertsCommand::List) | None => {
968            // Demo data
969            let alerts = vec![
970                AlertRow {
971                    id: "ALRT-001".to_string(),
972                    priority: format_priority(Priority::Critical),
973                    status: format_alert_status(&AlertStatus::Pending),
974                    survivor_id: "SURV-001".to_string(),
975                    title: "Immediate: Survivor detected".to_string(),
976                    age: "5m".to_string(),
977                },
978                AlertRow {
979                    id: "ALRT-002".to_string(),
980                    priority: format_priority(Priority::High),
981                    status: format_alert_status(&AlertStatus::Acknowledged),
982                    survivor_id: "SURV-002".to_string(),
983                    title: "Delayed: Survivor needs attention".to_string(),
984                    age: "12m".to_string(),
985                },
986            ];
987
988            let mut filtered = alerts;
989
990            if args.pending {
991                filtered.retain(|a| a.status.contains("Pending"));
992            }
993
994            if let Some(limit) = args.limit {
995                filtered.truncate(limit);
996            }
997
998            println!("{}", "Alerts".bold().cyan());
999            println!("{}", "=".repeat(100));
1000
1001            if filtered.is_empty() {
1002                println!("No alerts.");
1003            } else {
1004                let pending = filtered
1005                    .iter()
1006                    .filter(|a| a.status.contains("Pending"))
1007                    .count();
1008                if pending > 0 {
1009                    println!(
1010                        "{} {} pending alert(s) require attention!",
1011                        "[ALERT]".red().bold(),
1012                        pending
1013                    );
1014                    println!();
1015                }
1016
1017                let table = Table::new(filtered).with(Style::rounded()).to_string();
1018                println!("{}", table);
1019            }
1020        }
1021    }
1022
1023    Ok(())
1024}
1025
1026/// Execute the export command
1027async fn execute_export(args: ExportArgs) -> Result<()> {
1028    println!(
1029        "{} Exporting data to {}...",
1030        "[INFO]".blue(),
1031        args.output.display()
1032    );
1033
1034    // Demo export data
1035    #[derive(Serialize)]
1036    struct ExportData {
1037        exported_at: DateTime<Utc>,
1038        survivors: Vec<SurvivorExport>,
1039        zones: Vec<ZoneExport>,
1040        alerts: Vec<AlertExport>,
1041    }
1042
1043    #[derive(Serialize)]
1044    struct SurvivorExport {
1045        id: String,
1046        zone_id: String,
1047        triage_status: String,
1048        confidence: f64,
1049        location: Option<(f64, f64, f64)>,
1050        first_detected: DateTime<Utc>,
1051        last_updated: DateTime<Utc>,
1052    }
1053
1054    #[derive(Serialize)]
1055    struct ZoneExport {
1056        id: String,
1057        name: String,
1058        status: String,
1059        area: f64,
1060        scan_count: u32,
1061    }
1062
1063    #[derive(Serialize)]
1064    struct AlertExport {
1065        id: String,
1066        priority: String,
1067        status: String,
1068        survivor_id: String,
1069        created_at: DateTime<Utc>,
1070    }
1071
1072    let data = ExportData {
1073        exported_at: Utc::now(),
1074        survivors: vec![SurvivorExport {
1075            id: "SURV-001".to_string(),
1076            zone_id: "zone-001".to_string(),
1077            triage_status: "Immediate".to_string(),
1078            confidence: 0.92,
1079            location: Some((12.5, 8.3, -2.1)),
1080            first_detected: Utc::now() - chrono::Duration::minutes(15),
1081            last_updated: Utc::now() - chrono::Duration::seconds(30),
1082        }],
1083        zones: vec![ZoneExport {
1084            id: "zone-001".to_string(),
1085            name: "Building A - North Wing".to_string(),
1086            status: "Active".to_string(),
1087            area: 1500.0,
1088            scan_count: 42,
1089        }],
1090        alerts: vec![AlertExport {
1091            id: "ALRT-001".to_string(),
1092            priority: "Critical".to_string(),
1093            status: "Pending".to_string(),
1094            survivor_id: "SURV-001".to_string(),
1095            created_at: Utc::now() - chrono::Duration::minutes(5),
1096        }],
1097    };
1098
1099    match args.format {
1100        ExportFormat::Json => {
1101            let json = serde_json::to_string_pretty(&data)?;
1102            std::fs::write(&args.output, json)?;
1103        }
1104        ExportFormat::Csv => {
1105            let mut wtr = csv::Writer::from_path(&args.output)?;
1106            for survivor in &data.survivors {
1107                wtr.serialize(survivor)?;
1108            }
1109            wtr.flush()?;
1110        }
1111    }
1112
1113    println!(
1114        "{} Export complete: {}",
1115        "[OK]".green().bold(),
1116        args.output.display()
1117    );
1118
1119    Ok(())
1120}
1121
1122// ============================================================================
1123// Formatting Helpers
1124// ============================================================================
1125
1126/// Format triage status with color
1127fn format_triage(status: &TriageStatus) -> String {
1128    match status {
1129        TriageStatus::Immediate => "IMMEDIATE (Red)".red().bold().to_string(),
1130        TriageStatus::Delayed => "DELAYED (Yellow)".yellow().bold().to_string(),
1131        TriageStatus::Minor => "MINOR (Green)".green().bold().to_string(),
1132        TriageStatus::Deceased => "DECEASED (Black)".dimmed().to_string(),
1133        TriageStatus::Unknown => "UNKNOWN".dimmed().to_string(),
1134    }
1135}
1136
1137/// Format zone status with color
1138fn format_zone_status(status: &ZoneStatus) -> String {
1139    match status {
1140        ZoneStatus::Active => "Active".green().to_string(),
1141        ZoneStatus::Paused => "Paused".yellow().to_string(),
1142        ZoneStatus::Complete => "Complete".blue().to_string(),
1143        ZoneStatus::Inaccessible => "Inaccessible".red().to_string(),
1144        ZoneStatus::Deactivated => "Deactivated".dimmed().to_string(),
1145    }
1146}
1147
1148/// Format priority with color
1149fn format_priority(priority: Priority) -> String {
1150    match priority {
1151        Priority::Critical => "CRITICAL".red().bold().to_string(),
1152        Priority::High => "HIGH".bright_red().to_string(),
1153        Priority::Medium => "MEDIUM".yellow().to_string(),
1154        Priority::Low => "LOW".blue().to_string(),
1155    }
1156}
1157
1158/// Format alert status with color
1159fn format_alert_status(status: &AlertStatus) -> String {
1160    match status {
1161        AlertStatus::Pending => "Pending".red().to_string(),
1162        AlertStatus::Acknowledged => "Acknowledged".yellow().to_string(),
1163        AlertStatus::InProgress => "In Progress".blue().to_string(),
1164        AlertStatus::Resolved => "Resolved".green().to_string(),
1165        AlertStatus::Cancelled => "Cancelled".dimmed().to_string(),
1166        AlertStatus::Expired => "Expired".dimmed().to_string(),
1167    }
1168}
1169
1170#[cfg(test)]
1171mod tests {
1172    use super::*;
1173
1174    #[test]
1175    fn test_parse_rectangle_bounds() {
1176        let result = parse_bounds(&ZoneType::Rectangle, "0,0,10,20");
1177        assert!(result.is_ok());
1178    }
1179
1180    #[test]
1181    fn test_parse_circle_bounds() {
1182        let result = parse_bounds(&ZoneType::Circle, "5,5,10");
1183        assert!(result.is_ok());
1184    }
1185
1186    #[test]
1187    fn test_parse_invalid_bounds() {
1188        let result = parse_bounds(&ZoneType::Rectangle, "invalid");
1189        assert!(result.is_err());
1190    }
1191
1192    #[test]
1193    fn test_disaster_type_conversion() {
1194        let dt: DisasterType = DisasterTypeArg::Earthquake.into();
1195        assert!(matches!(dt, DisasterType::Earthquake));
1196    }
1197
1198    #[test]
1199    fn test_triage_filter_conversion() {
1200        let ts: TriageStatus = TriageFilter::Immediate.into();
1201        assert!(matches!(ts, TriageStatus::Immediate));
1202    }
1203}