use anyhow::Result;
use clap::{Parser, Subcommand};
use tracing_subscriber::{fmt, prelude::*, EnvFilter};

use biovault::cli;

use cli::commands;

// Validator for example names that also shows available examples
fn validate_example_name(s: &str) -> Result<String, String> {
    let examples = cli::examples::list_examples();
    if examples.contains(&s.to_string()) {
        Ok(s.to_string())
    } else {
        Err(format!(
            "Unknown example '{}'. Available examples: {}",
            s,
            examples.join(", ")
        ))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use clap::Parser;
    use tempfile::TempDir;

    struct EnvVarGuard {
        key: &'static str,
        previous: Option<String>,
    }

    impl EnvVarGuard {
        fn set(key: &'static str, value: &str) -> Self {
            let previous = std::env::var(key).ok();
            std::env::set_var(key, value);
            Self { key, previous }
        }
    }

    impl Drop for EnvVarGuard {
        fn drop(&mut self) {
            if let Some(prev) = &self.previous {
                std::env::set_var(self.key, prev);
            } else {
                std::env::remove_var(self.key);
            }
        }
    }

    struct TestHomeGuard {
        _temp: TempDir,
    }

    impl TestHomeGuard {
        fn new() -> Self {
            let temp = TempDir::new().unwrap();
            let home = temp.path().join(".biovault");
            std::fs::create_dir_all(&home).unwrap();
            biovault::config::set_test_biovault_home(&home);
            Self { _temp: temp }
        }
    }

    impl Drop for TestHomeGuard {
        fn drop(&mut self) {
            biovault::config::clear_test_biovault_home();
        }
    }

    #[test]
    fn validate_example_name_accepts_known_and_rejects_unknown() {
        let list = cli::examples::list_examples();
        // When at least one example exists, it validates
        if let Some(first) = list.first() {
            assert!(validate_example_name(first).is_ok());
        }
        // Unknown example returns Err with helpful message
        let err = validate_example_name("__definitely_not_real__").unwrap_err();
        assert!(err.contains("Unknown example"));
    }

    #[test]
    fn clap_parses_init_command_with_flags() {
        let cli = Cli::parse_from(["bv", "init", "--quiet", "user@example.com", "--verbose"]);
        assert!(cli.verbose);
        match cli.command {
            Commands::Init { email, quiet } => {
                assert_eq!(email.as_deref(), Some("user@example.com"));
                assert!(quiet);
            }
            _ => panic!("unexpected command variant"),
        }
    }

    #[test]
    fn clap_parses_run_command_defaults() {
        // Flags must come BEFORE trailing args due to trailing_var_arg = true
        let cli = Cli::parse_from([
            "bv",
            "run",
            "--dry-run",
            "--results-dir",
            "out",
            "project-dir",
            "--rows",
            "data.csv",
        ]);
        match cli.command {
            Commands::Run {
                project_folder,
                args,
                dry_run,
                resume,
                results_dir,
            } => {
                assert_eq!(project_folder, "project-dir");
                assert_eq!(args, vec!["--rows", "data.csv"]);
                assert!(dry_run);
                assert!(!resume);
                assert_eq!(results_dir.as_deref(), Some("out"));
            }
            _ => panic!("unexpected command variant"),
        }
    }

    #[test]
    fn clap_parses_run_command_with_args() {
        let cli = Cli::parse_from([
            "bv",
            "run",
            "project-dir",
            "--rows",
            "data.csv",
            "--param.threshold",
            "0.05",
        ]);
        match cli.command {
            Commands::Run {
                project_folder,
                args,
                ..
            } => {
                assert_eq!(project_folder, "project-dir");
                assert_eq!(args.len(), 4);
                assert_eq!(args[0], "--rows");
                assert_eq!(args[1], "data.csv");
                assert_eq!(args[2], "--param.threshold");
                assert_eq!(args[3], "0.05");
            }
            _ => panic!("unexpected command variant"),
        }
    }

    #[test]
    fn clap_parses_sample_data_list_subcommand() {
        let cli = Cli::parse_from(["bv", "sample-data", "list"]);
        match cli.command {
            Commands::SampleData { command } => match command {
                SampleDataCommands::List => {}
                _ => panic!("expected List variant"),
            },
            _ => panic!("unexpected command variant"),
        }
    }

    #[test]
    fn clap_parses_submit_command_with_flags() {
        let cli = Cli::parse_from([
            "bv",
            "submit",
            "./project",
            "friend@example.com",
            "--non-interactive",
            "--force",
        ]);
        match cli.command {
            Commands::Submit {
                project_path,
                destination,
                non_interactive,
                force,
            } => {
                assert_eq!(project_path, "./project");
                assert_eq!(destination, "friend@example.com");
                assert!(non_interactive);
                assert!(force);
            }
            _ => panic!("unexpected command variant"),
        }
    }

    #[test]
    fn clap_parses_inbox_filters() {
        let cli = Cli::parse_from([
            "bv",
            "inbox",
            "--plain",
            "--sent",
            "--message-type",
            "project",
            "--from",
            "alice@example.com",
            "--search",
            "urgent",
        ]);
        match cli.command {
            Commands::Inbox {
                interactive,
                plain,
                json,
                sent,
                all,
                unread,
                projects,
                message_type,
                from,
                search,
            } => {
                assert!(!interactive);
                assert!(plain);
                assert!(!json);
                assert!(sent);
                assert!(!all);
                assert!(!unread);
                assert!(!projects);
                assert_eq!(message_type.as_deref(), Some("project"));
                assert_eq!(from.as_deref(), Some("alice@example.com"));
                assert_eq!(search.as_deref(), Some("urgent"));
            }
            _ => panic!("unexpected command variant"),
        }
    }

    #[test]
    fn clap_parses_project_examples_subcommand() {
        let cli = Cli::parse_from(["bv", "project", "examples"]);
        match cli.command {
            Commands::Project { command } => match command {
                ProjectCommands::Examples => {}
                _ => panic!("expected Examples variant"),
            },
            _ => panic!("unexpected command variant"),
        }
    }

    #[test]
    fn async_main_with_sample_data_list_executes() {
        let _home_guard = TestHomeGuard::new();
        let _skip_guard = EnvVarGuard::set("BIOVAULT_SKIP_UPDATE_CHECK", "1");
        let runtime = tokio::runtime::Runtime::new().unwrap();

        let cli = Cli {
            command: Commands::SampleData {
                command: SampleDataCommands::List,
            },
            verbose: false,
            config: None,
        };

        runtime
            .block_on(async { super::async_main_with(cli).await })
            .unwrap();
    }

    #[test]
    fn async_main_with_info_executes() {
        let _home_guard = TestHomeGuard::new();
        let _skip_guard = EnvVarGuard::set("BIOVAULT_SKIP_UPDATE_CHECK", "1");
        let runtime = tokio::runtime::Runtime::new().unwrap();

        let cli = Cli {
            command: Commands::Info { json: false },
            verbose: false,
            config: None,
        };

        runtime
            .block_on(async { super::async_main_with(cli).await })
            .unwrap();
    }

    #[test]
    fn async_main_with_participant_list_executes() {
        let _home_guard = TestHomeGuard::new();
        let _skip_guard = EnvVarGuard::set("BIOVAULT_SKIP_UPDATE_CHECK", "1");
        let runtime = tokio::runtime::Runtime::new().unwrap();

        let cli = Cli {
            command: Commands::Participant {
                command: ParticipantCommands::List,
            },
            verbose: false,
            config: None,
        };

        runtime
            .block_on(async { super::async_main_with(cli).await })
            .unwrap();
    }

    #[test]
    fn async_main_with_project_examples_executes() {
        let _home_guard = TestHomeGuard::new();
        let _skip_guard = EnvVarGuard::set("BIOVAULT_SKIP_UPDATE_CHECK", "1");
        let runtime = tokio::runtime::Runtime::new().unwrap();

        let cli = Cli {
            command: Commands::Project {
                command: ProjectCommands::Examples,
            },
            verbose: false,
            config: None,
        };

        runtime
            .block_on(async { super::async_main_with(cli).await })
            .unwrap();
    }

    #[test]
    fn test_env_var_guard_restores_previous() {
        let key = "TEST_VAR_GUARD";
        std::env::set_var(key, "original");

        {
            let _guard = EnvVarGuard::set(key, "temp");
            assert_eq!(std::env::var(key).unwrap(), "temp");
        }

        assert_eq!(std::env::var(key).unwrap(), "original");
        std::env::remove_var(key);
    }

    #[test]
    fn test_env_var_guard_removes_if_not_set() {
        let key = "TEST_VAR_GUARD_REMOVE";
        std::env::remove_var(key);

        {
            let _guard = EnvVarGuard::set(key, "temp");
            assert_eq!(std::env::var(key).unwrap(), "temp");
        }

        assert!(std::env::var(key).is_err());
    }
}

#[derive(Parser)]
#[command(
    name = "bv",
    version,
    about = "BioVault - A bioinformatics data management CLI",
    long_about = None
)]
struct Cli {
    #[command(subcommand)]
    command: Commands,

    #[arg(short, long, global = true, help = "Increase verbosity")]
    verbose: bool,

    #[arg(long, global = true, help = "Path to config file")]
    config: Option<String>,
}

#[derive(Subcommand)]
enum Commands {
    #[command(about = "Check for updates and install the latest version")]
    Update,
    #[command(about = "Initialize a new BioVault repository")]
    Init {
        #[arg(
            help = "Email address for the vault configuration (optional, will detect from SYFTBOX_EMAIL)"
        )]
        email: Option<String>,

        #[arg(short, long, help = "Automatically accept defaults (for testing)")]
        quiet: bool,
    },

    #[command(about = "Show system information")]
    Info {
        #[arg(long, help = "Output plain mode as JSON")]
        json: bool,
    },

    #[command(about = "Check for required dependencies")]
    Check {
        #[arg(long, help = "Output plain mode as JSON")]
        json: bool,
    },

    #[command(about = "Setup environment for known systems (e.g., Google Colab)")]
    Setup {
        #[arg(
            help = "Specific dependencies to install (java, docker, nextflow, syftbox, uv). If not specified, installs all."
        )]
        dependencies: Vec<String>,

        #[arg(long, help = "Force reinstallation even if already installed")]
        force: bool,
    },

    #[command(about = "Project management commands")]
    Project {
        #[command(subcommand)]
        command: ProjectCommands,
    },

    #[command(about = "Pipeline authoring commands")]
    Pipeline {
        #[command(subcommand)]
        command: PipelineCommands,
    },

    #[command(about = "Run a project workflow with Nextflow")]
    Run {
        #[arg(help = "Path to project directory")]
        project_folder: String,

        #[arg(
            trailing_var_arg = true,
            allow_hyphen_values = true,
            help = "Input values, parameters, and Nextflow args (e.g., --rows data.csv --param.threshold 0.05)"
        )]
        args: Vec<String>,

        #[arg(long, help = "Show commands without executing")]
        dry_run: bool,

        #[arg(long, help = "Resume from previous run")]
        resume: bool,

        #[arg(long, help = "Custom results directory name")]
        results_dir: Option<String>,
    },

    #[command(name = "sample-data", about = "Manage sample data")]
    SampleData {
        #[command(subcommand)]
        command: SampleDataCommands,
    },

    #[command(about = "Manage participants")]
    Participant {
        #[command(subcommand)]
        command: ParticipantCommands,
    },

    #[command(about = "Manage participants in database catalog")]
    Participants {
        #[command(subcommand)]
        command: ParticipantsCommands,
    },

    #[command(about = "Manage biobank data publishing")]
    Biobank {
        #[command(subcommand)]
        command: BiobankCommands,
    },

    #[command(about = "Manage datasets and datacards")]
    Datasets {
        #[command(subcommand)]
        command: DatasetCommands,
    },

    #[command(about = "Manage BioVault configuration")]
    Config {
        #[command(subcommand)]
        command: Option<ConfigCommands>,

        #[arg(long, help = "Output plain mode as JSON")]
        json: bool,
    },

    #[command(about = "Manage SyftBox authentication")]
    Syftbox {
        #[command(subcommand)]
        command: SyftboxCommands,
    },

    #[command(about = "Manage Syft Crypto keys and bundles")]
    Key {
        #[command(subcommand)]
        command: commands::key::KeyCommands,
    },

    #[command(about = "Manage Syft Crypto vaults and bundles")]
    Syc {
        #[command(subcommand)]
        command: commands::syc::SycCommands,
    },

    #[command(about = "Manage contacts (trusted peers)")]
    Contacts {
        #[command(subcommand)]
        command: commands::contacts::ContactsCommands,
    },

    #[command(about = "Network discovery and status")]
    Network {
        #[command(subcommand)]
        command: commands::network::NetworkCommands,
    },

    #[command(about = "Manage collaborative Jupyter sessions")]
    Session {
        #[command(subcommand)]
        command: commands::sessions::SessionsCommands,
    },

    #[command(about = "FASTQ file operations")]
    Fastq {
        #[command(subcommand)]
        command: FastqCommands,
    },

    #[command(about = "Manage files and file catalogs")]
    Files {
        #[command(subcommand)]
        command: FilesCommands,
    },

    #[command(about = "SQL database operations")]
    Sql {
        #[command(subcommand)]
        command: SqlCommands,
    },

    #[command(about = "Submit a project to another biobank via SyftBox")]
    Submit {
        #[arg(help = "Path to project directory (use '.' for current directory)")]
        project_path: String,

        #[arg(
            help = "Destination: either a datasite email (e.g., user@domain.com) or full Syft URL (e.g., syft://user@domain.com/public/biovault/participants.yaml#participants.ID)"
        )]
        destination: String,

        #[arg(long, help = "Skip interactive prompts, use defaults")]
        non_interactive: bool,

        #[arg(
            long,
            help = "Force resubmission even if project was already submitted"
        )]
        force: bool,
    },

    #[command(about = "Clean up stale database locks")]
    Cleanup {
        #[arg(long, help = "Clean all locks in all virtualenvs")]
        all: bool,
    },

    #[command(about = "View and manage inbox messages")]
    Inbox {
        #[arg(short = 'i', long, help = "Interactive mode (default)")]
        interactive: bool,

        #[arg(long, help = "Plain, non-interactive list output")]
        plain: bool,

        #[arg(long, help = "Output plain mode as JSON")]
        json: bool,

        #[arg(short = 's', long, help = "Show sent messages")]
        sent: bool,

        #[arg(short = 'a', long, help = "Show all messages (including deleted)")]
        all: bool,

        #[arg(short = 'u', long, help = "Show only unread messages")]
        unread: bool,

        #[arg(short = 'p', long, help = "Show project submissions")]
        projects: bool,

        #[arg(
            short = 't',
            long,
            help = "Filter by message type (text/project/request)"
        )]
        message_type: Option<String>,

        #[arg(short = 'f', long, help = "Filter by sender")]
        from: Option<String>,

        #[arg(long, help = "Search messages by content")]
        search: Option<String>,
    },

    #[command(about = "Manage messages via SyftBox RPC")]
    Message {
        #[command(subcommand)]
        command: MessageCommands,
    },

    #[command(about = "Sample sheet operations")]
    Samplesheet {
        #[command(subcommand)]
        command: SamplesheetCommands,
    },

    #[command(about = "Manage the BioVault daemon for automatic message processing")]
    Daemon {
        #[command(subcommand)]
        command: DaemonCommands,
    },

    #[command(about = "Manage Python versions via UV")]
    Python {
        #[command(subcommand)]
        command: PythonCommands,
    },

    #[command(about = "Manage Jupyter Lab for projects")]
    Jupyter {
        #[command(subcommand)]
        command: JupyterCommands,
    },

    #[command(
        name = "hard-reset",
        about = "Delete all BioVault data and configuration (DESTRUCTIVE)"
    )]
    HardReset {
        #[arg(long, help = "Skip confirmation prompts (use with caution)")]
        ignore_warning: bool,
    },

    #[command(about = "Launch BioVault Desktop GUI")]
    Desktop {
        #[arg(long, help = "Path to BioVault config directory")]
        config: Option<String>,
    },
}

#[derive(Subcommand)]
enum DaemonCommands {
    #[command(about = "Start the BioVault daemon")]
    Start {
        #[arg(long, help = "Run daemon in foreground (no background)")]
        foreground: bool,
    },

    #[command(about = "Stop the running daemon")]
    Stop,

    #[command(about = "Restart the daemon (stop if running, then start)")]
    Restart {
        #[arg(long, help = "Run daemon in foreground after restart")]
        foreground: bool,
    },

    #[command(about = "Check daemon status")]
    Status,

    #[command(about = "View daemon logs")]
    Logs {
        #[arg(short, long, help = "Follow log output (tail -f)")]
        follow: bool,

        #[arg(short, long, help = "Number of lines to show (default: 50)")]
        lines: Option<usize>,
    },

    #[command(about = "Install daemon as a systemd service (Linux only)")]
    Install,

    #[command(about = "Uninstall daemon systemd service (Linux only)")]
    Uninstall,

    #[command(about = "List all installed daemon services")]
    List,

    #[command(about = "Show the systemd service file")]
    Show,

    #[command(about = "Reinstall daemon service (uninstall + install)")]
    Reinstall,
}

#[derive(Subcommand)]
enum ProjectCommands {
    #[command(about = "Create a new project")]
    Create {
        #[arg(long, help = "Project name")]
        name: Option<String>,

        #[arg(long, help = "Folder path (defaults to ./{name})")]
        folder: Option<String>,

        #[arg(long, value_parser = validate_example_name, help = "Use example template (use 'bv project examples' to list available)")]
        example: Option<String>,

        #[arg(long, help = "Scaffold from a project spec YAML file")]
        spec: Option<String>,

        #[arg(
            long,
            help = "Prepopulate outputs to feed into another project's inputs (pipeline composition)"
        )]
        input_to: Option<String>,

        #[arg(
            long,
            help = "Prepopulate inputs from another project's outputs (pipeline composition)"
        )]
        output_from: Option<String>,
    },

    #[command(about = "List available example templates")]
    Examples,

    #[command(about = "View project spec with ASCII diagram")]
    View {
        #[arg(help = "Project directory path (defaults to current directory)")]
        path: Option<String>,
    },

    #[command(about = "Import a project from URL or register a local project")]
    Import {
        #[arg(help = "URL to project.yaml or local directory path")]
        source: String,

        #[arg(long, help = "Override project name")]
        name: Option<String>,

        #[arg(long, help = "Overwrite existing project")]
        overwrite: bool,

        #[arg(long, help = "Output format (json|table)", default_value = "table")]
        format: String,
    },

    #[command(about = "List all registered projects")]
    List {
        #[arg(long, help = "Output format (json|table)", default_value = "table")]
        format: String,
    },

    #[command(about = "Show detailed information about a project")]
    Show {
        #[arg(help = "Project name or ID")]
        identifier: String,

        #[arg(long, help = "Output format (json|table)", default_value = "table")]
        format: String,
    },

    #[command(about = "Delete a project")]
    Delete {
        #[arg(help = "Project name or ID")]
        identifier: String,

        #[arg(long, help = "Keep project files, only remove from database")]
        keep_files: bool,

        #[arg(long, help = "Output format (json|table)", default_value = "table")]
        format: String,
    },
}

#[derive(Subcommand)]
enum PipelineCommands {
    #[command(about = "Create a new pipeline interactively")]
    Create {
        #[arg(long, help = "Pipeline YAML file path (defaults to pipeline.yaml)")]
        file: Option<String>,

        #[arg(long, help = "Pipeline name (non-interactive mode)")]
        name: Option<String>,

        #[arg(
            long = "use-project",
            help = "Project path or project.yaml to add as the first step (non-interactive)"
        )]
        uses: Option<String>,

        #[arg(long = "step-id", help = "Override the generated step id")]
        step_id: Option<String>,
    },

    #[command(about = "Add a step to an existing pipeline")]
    AddStep {
        #[arg(long, help = "Pipeline YAML file path (defaults to pipeline.yaml)")]
        file: Option<String>,
    },

    #[command(about = "Validate a pipeline spec")]
    Validate {
        #[arg(help = "Pipeline YAML file path")]
        file: String,

        #[arg(long, help = "Render step diagram after validation")]
        diagram: bool,
    },

    #[command(about = "Print pipeline steps and bindings")]
    Inspect {
        #[arg(help = "Pipeline YAML file path")]
        file: String,
    },
}

#[derive(Subcommand)]
enum SampleDataCommands {
    #[command(about = "Fetch sample data")]
    Fetch {
        #[arg(
            value_delimiter = ',',
            help = "Participant IDs to fetch (comma-separated)"
        )]
        participant_ids: Option<Vec<String>>,

        #[arg(long, help = "Fetch all available sample data")]
        all: bool,
    },

    #[command(about = "List available sample data")]
    List,
}

#[derive(Subcommand)]
enum ParticipantCommands {
    #[command(about = "Add a new participant")]
    Add {
        #[arg(long, help = "Participant ID")]
        id: Option<String>,

        #[arg(long, help = "Aligned file path (.cram, .bam, or .sam)")]
        aligned: Option<String>,

        #[arg(
            long,
            help = "Template type (default or snp)",
            default_value = "default"
        )]
        template: Option<String>,

        #[arg(long, help = "SNP file path (for SNP template)")]
        snp: Option<String>,

        #[arg(long, help = "Reference genome file path (.fa or .fasta)")]
        reference: Option<String>,

        #[arg(long, help = "Reference version (GRCh38 or GRCh37)")]
        ref_version: Option<String>,

        #[arg(long, help = "Skip interactive prompts, use defaults")]
        non_interactive: bool,
    },

    #[command(about = "List all participants")]
    List,

    #[command(about = "Delete a participant")]
    Delete {
        #[arg(help = "Participant ID to delete")]
        id: String,
    },

    #[command(about = "Validate participant files")]
    Validate {
        #[arg(help = "Participant ID to validate (validates all if not specified)")]
        id: Option<String>,
    },
}

#[derive(Subcommand)]
enum ParticipantsCommands {
    #[command(about = "List all participants in the catalog")]
    List {
        #[arg(long, help = "Output format (json|table)", default_value = "table")]
        format: String,
    },

    #[command(about = "Delete a participant and unlink all files")]
    Delete {
        #[arg(help = "Participant database ID")]
        id: i64,

        #[arg(long, help = "Output format (json|table)", default_value = "table")]
        format: String,
    },

    #[command(about = "Delete multiple participants and remove their files")]
    DeleteBulk {
        #[arg(
            help = "Participant database IDs to delete (comma-separated)",
            value_delimiter = ','
        )]
        ids: Vec<i64>,

        #[arg(long, help = "Output format (json|table)", default_value = "table")]
        format: String,
    },
}

#[derive(Subcommand)]
enum BiobankCommands {
    #[command(about = "List biobanks in SyftBox")]
    List,

    #[command(about = "Publish participants to SyftBox")]
    Publish {
        #[arg(long, help = "Participant ID to publish")]
        participant_id: Option<String>,

        #[arg(long, help = "Publish all participants")]
        all: bool,

        #[arg(
            long,
            help = "HTTP relay servers (defaults to syftbox.net)",
            value_delimiter = ','
        )]
        http_relay_servers: Option<Vec<String>>,
    },

    #[command(about = "Unpublish participants from SyftBox")]
    Unpublish {
        #[arg(long, help = "Participant ID to unpublish")]
        participant_id: Option<String>,

        #[arg(long, help = "Unpublish all participants")]
        all: bool,
    },
}

#[derive(Subcommand)]
enum DatasetCommands {
    #[command(about = "Publish a dataset manifest to SyftBox")]
    Publish {
        #[arg(
            help = "Path to dataset manifest YAML",
            required_unless_present = "name"
        )]
        manifest: Option<String>,

        #[arg(
            long,
            help = "Publish from dataset stored in DB by name",
            conflicts_with = "manifest"
        )]
        name: Option<String>,

        #[arg(
            long,
            help = "Copy mock artifacts referenced in the manifest into the public dataset folder"
        )]
        copy_mock: bool,
    },

    #[command(about = "List datasets published to SyftBox")]
    List,

    #[command(about = "Generate a dataset manifest for editing")]
    Init {
        #[arg(help = "Dataset name")]
        name: String,

        #[arg(help = "Path to private asset file", required = false)]
        private: Option<String>,

        #[arg(
            long = "private-id",
            help = "File ID from the catalog to use as private asset"
        )]
        private_id: Option<i64>,

        #[arg(long, help = "Path to mock asset file")]
        mock: Option<String>,

        #[arg(
            long = "mock-id",
            help = "File ID from the catalog to use as mock asset"
        )]
        mock_id: Option<i64>,

        #[arg(long, help = "Description for the dataset")]
        description: Option<String>,

        #[arg(long, help = "Author (defaults to your datasite email)")]
        author: Option<String>,

        #[arg(long, help = "Version (default: 1.0.0)")]
        version: Option<String>,

        #[arg(
            long = "asset-key",
            help = "Key for the asset map (default: derived from file name)"
        )]
        asset_key: Option<String>,

        #[arg(long = "asset-type", help = "Asset type (twin|file, default: twin)")]
        asset_type: Option<String>,

        #[arg(long, help = "Output path for generated manifest")]
        output: Option<String>,
    },
}

#[derive(Subcommand)]
enum ConfigCommands {
    #[command(about = "Set email address")]
    Email {
        #[arg(help = "Email address")]
        email: String,
    },

    #[command(about = "Set SyftBox config path")]
    Syftbox {
        #[arg(help = "Path to SyftBox config.json (omit to use default)")]
        path: Option<String>,
    },
}

#[derive(Subcommand)]
enum SyftboxCommands {
    #[command(about = "Request a SyftBox OTP email")]
    RequestOtp {
        #[arg(long, help = "Email address to send the OTP to")]
        email: Option<String>,
        #[arg(long, help = "Display name to include in the request")]
        name: Option<String>,
        #[arg(long, help = "SyftBox server base URL")]
        server: Option<String>,
    },

    #[command(about = "Submit an OTP and store SyftBox tokens")]
    SubmitOtp {
        #[arg(help = "OTP code received via email")]
        code: String,
        #[arg(long, help = "Email address associated with the OTP")]
        email: Option<String>,
        #[arg(long, help = "SyftBox server base URL")]
        server: Option<String>,
        #[arg(long, help = "Path to syftbox config.json to update")]
        config: Option<String>,
        #[arg(long, help = "SyftBox data directory to embed in config")]
        data_dir: Option<String>,
        #[arg(long, help = "SyftBox client URL to store in config")]
        client_url: Option<String>,
    },
}

#[derive(Subcommand)]
enum FastqCommands {
    #[command(about = "Combine multiple FASTQ files into one")]
    Combine {
        #[arg(help = "Input folder containing FASTQ files")]
        input_folder: String,

        #[arg(help = "Output file path")]
        output_file: String,

        #[arg(long, help = "Validate files before combining")]
        validate: bool,

        #[arg(long, help = "Skip validation prompt and use default")]
        no_prompt: bool,

        #[arg(
            long,
            default_value = "tsv",
            help = "Stats output format (tsv, yaml, json)"
        )]
        stats_format: String,
    },
}

#[derive(Subcommand)]
enum FilesCommands {
    #[command(about = "Scan directory for files by type")]
    Scan {
        #[arg(help = "Directory path to scan")]
        path: String,

        #[arg(long, help = "File extension filter (e.g., .vcf, .cram)")]
        ext: Option<String>,

        #[arg(long, help = "Scan subdirectories recursively", default_value = "true")]
        recursive: bool,

        #[arg(long, help = "Output format (json|table)", default_value = "table")]
        format: String,
    },

    #[command(about = "Import files into database catalog")]
    Import {
        #[arg(help = "File path or directory to import")]
        path: String,

        #[arg(long, help = "File extension filter (e.g., .vcf, .cram)")]
        ext: Option<String>,

        #[arg(
            long,
            help = "Import subdirectories recursively",
            default_value = "true"
        )]
        recursive: bool,

        #[arg(
            long,
            help = "Pattern to extract participant IDs (e.g., 'case_{id}_*')"
        )]
        pattern: Option<String>,

        #[arg(long, help = "Dry run - show what would be imported without importing")]
        dry_run: bool,

        #[arg(long, help = "Skip interactive confirmation prompt")]
        non_interactive: bool,

        #[arg(long, help = "Output format (json|table)", default_value = "table")]
        format: String,
    },

    #[command(about = "List cataloged files with filtering")]
    List {
        #[arg(long, help = "Filter by file extension")]
        ext: Option<String>,

        #[arg(long, help = "Filter by participant ID")]
        participant: Option<String>,

        #[arg(long, help = "Show only files without participant assignment")]
        unassigned: bool,

        #[arg(long, help = "Maximum number of results")]
        limit: Option<usize>,

        #[arg(long, help = "Output format (json|table)", default_value = "table")]
        format: String,
    },

    #[command(about = "Show detailed file information")]
    Info {
        #[arg(help = "File ID or path")]
        file: String,

        #[arg(long, help = "Output format (json|table)", default_value = "table")]
        format: String,
    },

    #[command(about = "Suggest participant ID patterns from filenames")]
    SuggestPatterns {
        #[arg(help = "Directory path to analyze")]
        path: String,

        #[arg(long, help = "File extension filter (e.g., .vcf, .cram)")]
        ext: Option<String>,

        #[arg(long, help = "Scan subdirectories recursively", default_value = "true")]
        recursive: bool,

        #[arg(long, help = "Output format (json|table)", default_value = "table")]
        format: String,
    },

    #[command(about = "Delete a file record from the catalog")]
    Delete {
        #[arg(help = "File ID to delete")]
        id: i64,

        #[arg(long, help = "Output format (json|table)", default_value = "table")]
        format: String,
    },

    #[command(about = "Delete multiple file records from the catalog")]
    DeleteBulk {
        #[arg(help = "File IDs to delete (comma-separated)", value_delimiter = ',')]
        ids: Vec<i64>,

        #[arg(long, help = "Output format (json|table)", default_value = "table")]
        format: String,
    },

    #[command(about = "Detect file types and extract genotype metadata (fast - header only)")]
    Detect {
        #[arg(help = "File paths to analyze", num_args = 1..)]
        files: Vec<String>,

        #[arg(long, help = "Output format (json|table)", default_value = "table")]
        format: String,
    },

    #[command(
        about = "Analyze genotype files for row count, chromosomes, and sex (slow - reads entire file)"
    )]
    Analyze {
        #[arg(help = "File paths to analyze", num_args = 1..)]
        files: Vec<String>,

        #[arg(long, help = "Output format (json|table)", default_value = "table")]
        format: String,
    },

    #[command(about = "Link a file to a participant")]
    Link {
        #[arg(help = "File ID to link")]
        file_id: i64,

        #[arg(long, help = "Participant ID to link to")]
        participant: String,

        #[arg(long, help = "Output format (json|table)", default_value = "table")]
        format: String,
    },

    #[command(about = "Bulk link files to participants via JSON")]
    LinkBulk {
        #[arg(help = "JSON object mapping file paths to participant IDs")]
        json: String,

        #[arg(long, help = "Output format (json|table)", default_value = "table")]
        format: String,
    },

    #[command(about = "Remove participant assignment from a file")]
    Unlink {
        #[arg(help = "File ID to unlink")]
        file_id: i64,

        #[arg(long, help = "Output format (json|table)", default_value = "table")]
        format: String,
    },

    #[command(about = "Calculate BLAKE3 hashes for files")]
    Hash {
        #[arg(help = "File paths to hash", num_args = 1..)]
        files: Vec<String>,

        #[arg(long, help = "Output format (json|table)", default_value = "table")]
        format: String,
    },

    #[command(about = "Export files to CSV with participant IDs")]
    ExportCsv {
        #[arg(help = "Directory path to scan")]
        path: String,

        #[arg(long, help = "File extension filter (e.g., .vcf, .txt)")]
        ext: Option<String>,

        #[arg(long, help = "Scan subdirectories recursively", default_value = "true")]
        recursive: bool,

        #[arg(long, help = "Pattern to extract participant IDs (e.g., '{parent}')")]
        pattern: Option<String>,

        #[arg(short = 'o', long, help = "Output CSV file path")]
        output: String,
    },

    #[command(about = "Detect file types in CSV and update metadata columns")]
    DetectCsv {
        #[arg(help = "Input CSV file path")]
        input_csv: String,

        #[arg(short = 'o', long, help = "Output CSV file path")]
        output: String,
    },

    #[command(about = "Analyze genotype files in CSV for row counts and sex")]
    AnalyzeCsv {
        #[arg(help = "Input CSV file path")]
        input_csv: String,

        #[arg(short = 'o', long, help = "Output CSV file path")]
        output: String,
    },

    #[command(about = "Import files from CSV with complete metadata")]
    ImportCsv {
        #[arg(help = "Path to CSV file containing file metadata")]
        csv_path: String,

        #[arg(
            long,
            help = "Skip interactive data-quality wizard",
            default_value = "false"
        )]
        non_interactive: bool,

        #[arg(long, help = "Output format (json|table)", default_value = "table")]
        format: String,

        #[arg(long, help = "Save skipped rows to a CSV file")]
        save_skipped: Option<String>,
    },

    #[command(about = "Process pending files in the queue (hash and detect metadata)")]
    ProcessQueue {
        #[arg(
            long,
            help = "Maximum number of files to process",
            default_value = "100"
        )]
        limit: usize,

        #[arg(long, help = "Daemon mode - continuously process queue")]
        daemon: bool,

        #[arg(long, help = "Output format (json|table)", default_value = "table")]
        format: String,
    },

    #[command(about = "Fast import - add files to queue for background processing")]
    ImportPending {
        #[arg(help = "Path to CSV file containing file metadata")]
        csv_path: String,

        #[arg(long, help = "Output format (json|table)", default_value = "table")]
        format: String,
    },
}

#[derive(Subcommand)]
enum SamplesheetCommands {
    #[command(about = "Create a sample sheet CSV from a folder of files")]
    Create {
        #[arg(help = "Input directory containing files")]
        input_dir: String,

        #[arg(help = "Output CSV file path")]
        output_file: String,

        #[arg(
            long = "file_filter",
            help = "File pattern filter (e.g., *.txt, default: all files)"
        )]
        file_filter: Option<String>,

        #[arg(
            long = "extract_cols",
            help = "Pattern for extracting fields from filenames (e.g., {participant_id}_X_X_GSAv3-DTC_GRCh38-{date}.txt)"
        )]
        extract_cols: Option<String>,

        #[arg(
            long = "ignore",
            help = "Add files even if they don't match the extraction pattern"
        )]
        ignore: bool,
    },
    #[command(about = "Export cataloged files into a samplesheet CSV")]
    ExportCatalog {
        #[arg(help = "Output CSV file path")]
        output_file: String,

        #[arg(
            long = "data-type",
            default_value = "genotype",
            help = "Filter catalog entries by data type (use 'all' for no filter)"
        )]
        data_type: String,

        #[arg(
            long = "participants",
            help = "Comma-separated participant IDs to include (default: all)"
        )]
        participants: Option<String>,

        #[arg(
            long = "path-column",
            default_value = "genotype_file",
            help = "Column name to use for file paths in the CSV"
        )]
        path_column: String,
    },
}

#[derive(Subcommand)]
enum MessageCommands {
    #[command(about = "Send a message to another datasite")]
    Send {
        #[arg(help = "Recipient email address")]
        recipient: String,

        #[arg(help = "Message content")]
        message: String,

        #[arg(short = 's', long = "subject", help = "Optional message subject")]
        subject: Option<String>,
    },

    #[command(about = "Reply to a message")]
    Reply {
        #[arg(help = "Message ID to reply to")]
        message_id: String,

        #[arg(help = "Reply content")]
        body: String,
    },

    #[command(about = "Read a specific message")]
    Read {
        #[arg(help = "Message ID to read")]
        message_id: String,

        #[arg(long, help = "Skip interactive prompts when reading message")]
        non_interactive: bool,
    },

    #[command(about = "Delete a message")]
    Delete {
        #[arg(help = "Message ID to delete")]
        message_id: String,
    },

    #[command(about = "List messages")]
    List {
        #[arg(short = 'u', long = "unread", help = "Show only unread messages")]
        unread: bool,

        #[arg(short = 's', long = "sent", help = "Show sent messages")]
        sent: bool,

        #[arg(short = 'p', long = "projects", help = "Show only project messages")]
        projects: bool,

        #[arg(long, help = "Output messages as JSON")]
        json: bool,
    },

    #[command(about = "View a message thread")]
    Thread {
        #[arg(help = "Thread ID to view")]
        thread_id: String,
    },

    #[command(about = "Sync messages (check for new and update ACKs)")]
    Sync {
        #[arg(
            long,
            help = "Keep consumed messages for debugging (don't delete .request/.response files)"
        )]
        no_cleanup: bool,
    },

    #[command(about = "Process a project message (run test/real)")]
    Process {
        #[arg(help = "Message ID of the project to process")]
        message_id: String,

        #[arg(long, help = "Run with test data", conflicts_with = "real")]
        test: bool,

        #[arg(long, help = "Run with real data", conflicts_with = "test")]
        real: bool,

        #[arg(long, help = "Participant to use (defaults to first available)")]
        participant: Option<String>,

        #[arg(long, help = "Approve after successful run")]
        approve: bool,

        #[arg(long, help = "Non-interactive mode (skip prompts)")]
        non_interactive: bool,
    },

    #[command(about = "Archive a project message (revoke write permissions)")]
    Archive {
        #[arg(help = "Message ID to archive")]
        message_id: String,
    },
}

#[derive(Subcommand)]
enum PythonCommands {
    #[command(about = "Install a Python version via UV")]
    Install {
        #[arg(help = "Python version (e.g., 3.12, 3.11.6)")]
        version: String,
    },

    #[command(about = "List available or installed Python versions")]
    List {
        #[arg(long, help = "Show only installed versions")]
        installed: bool,
    },

    #[command(about = "Show default Python version")]
    Show,

    #[command(about = "Uninstall a UV-managed Python version")]
    Uninstall {
        #[arg(help = "Python version to uninstall")]
        version: String,
    },
}

#[derive(Subcommand)]
enum SqlCommands {
    #[command(about = "List all database tables")]
    Tables,

    #[command(about = "Show table structure/schema")]
    Structure {
        #[arg(help = "Table name (shows all tables if not specified)")]
        table: Option<String>,

        #[arg(long, help = "Output plain mode as JSON")]
        json: bool,
    },

    #[command(about = "Execute SQL query")]
    Run {
        #[arg(help = "SQL query to execute")]
        query: String,

        #[arg(short = 'o', long, help = "Export results to file")]
        output: Option<String>,

        #[arg(long, default_value = "csv", help = "Output format (csv|tsv|json)")]
        format: String,

        #[arg(long, help = "Allow write operations (INSERT, UPDATE, DELETE)")]
        allow_write: bool,

        #[arg(long, help = "Allow DDL operations (CREATE, DROP, ALTER)")]
        allow_ddl: bool,
    },

    #[command(about = "Import CSV/TSV data into database")]
    Import {
        #[arg(help = "Path to CSV/TSV file")]
        file_path: String,

        #[arg(long, help = "Target table name (will be prefixed with 'user_')")]
        table: String,

        #[arg(
            long,
            help = "Column containing participant_id (default: participant_id)"
        )]
        participant_col: Option<String>,

        #[arg(long, help = "Column mapping (e.g., 'old:new,foo:bar')")]
        map: Option<String>,

        #[arg(long, default_value = "csv", help = "File format (csv|tsv)")]
        format: String,

        #[arg(
            long,
            default_value = "error",
            help = "How to handle participant mismatches (error|skip|create)"
        )]
        on_mismatch: String,

        #[arg(long, help = "Dry run - validate without importing")]
        dry_run: bool,

        #[arg(long, help = "Allow overwriting existing user table")]
        allow_overwrite: bool,
    },

    #[command(about = "Drop a user table")]
    Drop {
        #[arg(help = "Table name to drop (must start with 'user_')")]
        table: String,

        #[arg(long, help = "Skip confirmation prompt")]
        confirm: bool,
    },
}

#[derive(Subcommand)]
enum JupyterCommands {
    #[command(about = "Start Jupyter Lab for a project")]
    Start {
        #[arg(help = "Project directory path")]
        project_path: String,

        #[arg(long, default_value = "3.12", help = "Python version to use")]
        python: String,
    },

    #[command(about = "Stop Jupyter Lab")]
    Stop {
        #[arg(help = "Project directory path")]
        project_path: String,
    },

    #[command(about = "Reset virtualenv and restart Jupyter")]
    Reset {
        #[arg(help = "Project directory path")]
        project_path: String,

        #[arg(long, default_value = "3.12", help = "Python version to use")]
        python: String,
    },

    #[command(about = "Show Jupyter status for projects")]
    Status,

    #[command(about = "List projects with Jupyter virtualenvs")]
    List,
}

fn main() -> Result<()> {
    // Set up panic hook to log crashes
    std::panic::set_hook(Box::new(|panic_info| {
        let payload = panic_info.payload();
        let message = if let Some(s) = payload.downcast_ref::<&str>() {
            s.to_string()
        } else if let Some(s) = payload.downcast_ref::<String>() {
            s.clone()
        } else {
            "Unknown panic payload".to_string()
        };

        let location = panic_info
            .location()
            .map(|loc| format!("{}:{}:{}", loc.file(), loc.line(), loc.column()))
            .unwrap_or_else(|| "unknown location".to_string());

        let crash_log = format!(
            "\n=== PANIC/CRASH DETECTED ===\n\
             Time: {}\n\
             Message: {}\n\
             Location: {}\n\
             Backtrace: use RUST_BACKTRACE=1 for details\n\
             ============================\n",
            chrono::Utc::now().format("%Y-%m-%d %H:%M:%S%.3f UTC"),
            message,
            location
        );

        // Try to write to daemon log if we're in daemon mode
        if let Ok(syftbox_dir) = std::env::var("SYFTBOX_DATA_DIR") {
            let log_path = std::path::PathBuf::from(&syftbox_dir)
                .join(".biovault")
                .join("logs")
                .join("daemon.log");

            if let Ok(mut file) = std::fs::OpenOptions::new()
                .create(true)
                .append(true)
                .open(&log_path)
            {
                use std::io::Write;
                let _ = writeln!(file, "{}", crash_log);
            }
        }

        // Also write to stderr
        eprintln!("{}", crash_log);
    }));

    // Use a minimal tokio runtime - this is just a file-watching daemon, not a web server
    // 2 worker threads is plenty for occasional I/O operations
    let runtime = tokio::runtime::Builder::new_multi_thread()
        .worker_threads(2)
        .thread_name("bv-worker")
        .enable_all()
        .build()
        .expect("Failed to create tokio runtime");

    runtime.block_on(async_main())
}

async fn async_main_with(cli: Cli) -> Result<()> {
    let filter_level = if cli.verbose { "debug" } else { "info" };

    // Send logs to stderr so stdout remains parseable for `--json` outputs and piping.
    let _ = tracing_subscriber::registry()
        .with(fmt::layer().with_writer(std::io::stderr))
        .with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(filter_level)))
        .try_init();

    // Ensure Syft Crypto uses the BioVault-managed vault path by default
    biovault::config::ensure_syc_vault_env()?;

    // Random version check on startup (10% chance)
    let _ = commands::update::check_and_notify_random().await;

    // Check for upgrades and perform them if needed
    let _ = cli::upgrade::check_and_upgrade();

    match cli.command {
        Commands::Update => {
            commands::update::execute().await?;
        }
        Commands::Init { email, quiet } => {
            commands::init::execute(email.as_deref(), quiet).await?;
        }
        Commands::Info { json } => {
            commands::info::execute(json).await?;
        }
        Commands::Check { json } => {
            commands::check::execute(json).await?;
        }
        Commands::Setup {
            dependencies,
            force,
        } => {
            commands::setup::execute(dependencies, force).await?;
        }
        Commands::Project { command } => match command {
            ProjectCommands::Create {
                name,
                folder,
                example,
                spec,
                input_to,
                output_from,
            } => {
                commands::project::create(name, folder, example, spec, input_to, output_from)
                    .await?;
            }
            ProjectCommands::Examples => {
                commands::project::list_examples()?;
            }
            ProjectCommands::View { path } => {
                commands::project::view(path)?;
            }
            ProjectCommands::Import {
                source,
                name,
                overwrite,
                format,
            } => {
                let fmt = if format == "table" {
                    None
                } else {
                    Some(format)
                };
                commands::project_management::import(source, name, overwrite, fmt).await?;
            }
            ProjectCommands::List { format } => {
                let fmt = if format == "table" {
                    None
                } else {
                    Some(format)
                };
                commands::project_management::list(fmt)?;
            }
            ProjectCommands::Show { identifier, format } => {
                let fmt = if format == "table" {
                    None
                } else {
                    Some(format)
                };
                commands::project_management::show(identifier, fmt)?;
            }
            ProjectCommands::Delete {
                identifier,
                keep_files,
                format,
            } => {
                let fmt = if format == "table" {
                    None
                } else {
                    Some(format)
                };
                commands::project_management::delete(identifier, keep_files, fmt)?;
            }
        },
        Commands::Pipeline { command } => match command {
            PipelineCommands::Create {
                file,
                name,
                uses,
                step_id,
            } => {
                commands::pipeline::create(file, name, uses, step_id).await?;
            }
            PipelineCommands::AddStep { file } => {
                commands::pipeline::add_step(file).await?;
            }
            PipelineCommands::Validate { file, diagram } => {
                commands::pipeline::validate(&file, diagram)?;
            }
            PipelineCommands::Inspect { file } => {
                commands::pipeline::inspect(&file)?;
            }
        },
        Commands::Run {
            project_folder,
            args,
            dry_run,
            resume,
            results_dir,
        } => {
            if project_folder.ends_with(".yaml") || project_folder.ends_with(".yml") {
                commands::pipeline::run_pipeline(
                    &project_folder,
                    args,
                    dry_run,
                    resume,
                    results_dir,
                )
                .await?;
                return Ok(());
            }
            // Check project template to determine which run system to use
            use std::path::Path;
            let project_path = Path::new(&project_folder);
            let project_yaml_path = project_path.join("project.yaml");

            if project_yaml_path.exists() {
                // Read project.yaml to check template
                if let Ok(content) = std::fs::read_to_string(&project_yaml_path) {
                    #[derive(serde::Deserialize)]
                    struct ProjectYaml {
                        #[serde(default)]
                        template: Option<String>,
                    }

                    if let Ok(project) = serde_yaml::from_str::<ProjectYaml>(&content) {
                        let template = project.template.as_deref().unwrap_or("default");

                        // Use dynamic run system for dynamic-nextflow template
                        if template == "dynamic-nextflow" {
                            commands::run_dynamic::execute_dynamic(
                                &project_folder,
                                args,
                                dry_run,
                                resume,
                                results_dir,
                                commands::run_dynamic::RunSettings::default(),
                            )
                            .await?;
                        } else {
                            // Use old run system for legacy templates (snp, sheet, default)
                            // Parse args in old format: [participant_source, flags...]
                            let mut participant_source = String::new();
                            let mut test = false;
                            let mut download = false;
                            let mut with_docker = true; // default to true for backward compatibility
                            let mut work_dir = None;
                            let mut nextflow_args = Vec::new();
                            let template_override = None;

                            // Parse args: first arg is participant_source, rest are flags
                            let mut i = 0;
                            while i < args.len() {
                                let arg = &args[i];
                                if arg.starts_with("--") {
                                    match arg.as_str() {
                                        "--test" => test = true,
                                        "--download" => download = true,
                                        "--with-docker" => with_docker = true,
                                        "--no-docker" => with_docker = false,
                                        "--work-dir" => {
                                            if i + 1 < args.len() {
                                                work_dir = Some(args[i + 1].clone());
                                                i += 1;
                                            }
                                        }
                                        _ => {
                                            // Nextflow args
                                            nextflow_args.push(arg.clone());
                                            if !arg.starts_with("--") && i + 1 < args.len() {
                                                nextflow_args.push(args[i + 1].clone());
                                                i += 1;
                                            }
                                        }
                                    }
                                } else if participant_source.is_empty() {
                                    participant_source = arg.clone();
                                }
                                i += 1;
                            }

                            commands::run::execute(commands::run::RunParams {
                                project_folder: project_folder.clone(),
                                participant_source,
                                test,
                                download,
                                dry_run,
                                with_docker,
                                work_dir,
                                resume,
                                template: template_override,
                                results_dir,
                                nextflow_args,
                            })
                            .await?;
                        }
                    } else {
                        // Couldn't parse project.yaml, default to dynamic
                        commands::run_dynamic::execute_dynamic(
                            &project_folder,
                            args,
                            dry_run,
                            resume,
                            results_dir,
                            commands::run_dynamic::RunSettings::default(),
                        )
                        .await?;
                    }
                } else {
                    // Couldn't read project.yaml, default to dynamic
                    commands::run_dynamic::execute_dynamic(
                        &project_folder,
                        args,
                        dry_run,
                        resume,
                        results_dir,
                        commands::run_dynamic::RunSettings::default(),
                    )
                    .await?;
                }
            } else {
                // No project.yaml, use dynamic
                commands::run_dynamic::execute_dynamic(
                    &project_folder,
                    args,
                    dry_run,
                    resume,
                    results_dir,
                    commands::run_dynamic::RunSettings::default(),
                )
                .await?;
            }
        }
        Commands::SampleData { command } => match command {
            SampleDataCommands::Fetch {
                participant_ids,
                all,
            } => {
                commands::sample_data::fetch(participant_ids, all, false).await?;
            }
            SampleDataCommands::List => {
                commands::sample_data::list().await?;
            }
        },
        Commands::Participant { command } => match command {
            ParticipantCommands::Add {
                id,
                aligned,
                template,
                snp,
                reference,
                ref_version,
                non_interactive,
            } => {
                commands::participant::add(
                    id,
                    aligned,
                    template,
                    snp,
                    reference,
                    ref_version,
                    non_interactive,
                )
                .await?;
            }
            ParticipantCommands::List => {
                commands::participant::list().await?;
            }
            ParticipantCommands::Delete { id } => {
                commands::participant::delete(id).await?;
            }
            ParticipantCommands::Validate { id } => {
                commands::participant::validate(id).await?;
            }
        },
        Commands::Participants { command } => match command {
            ParticipantsCommands::List { format } => {
                commands::participants::list(format).await?;
            }
            ParticipantsCommands::Delete { id, format } => {
                commands::participants::delete(id, format).await?;
            }
            ParticipantsCommands::DeleteBulk { ids, format } => {
                commands::participants::delete_bulk(ids, format).await?;
            }
        },
        Commands::Biobank { command } => match command {
            BiobankCommands::List => {
                commands::biobank::list(None).await?;
            }
            BiobankCommands::Publish {
                participant_id,
                all,
                http_relay_servers,
            } => {
                commands::biobank::publish(participant_id, all, http_relay_servers).await?;
            }
            BiobankCommands::Unpublish {
                participant_id,
                all,
            } => {
                commands::biobank::unpublish(participant_id, all).await?;
            }
        },
        Commands::Datasets { command } => match command {
            DatasetCommands::Publish {
                manifest,
                name,
                copy_mock,
            } => {
                commands::datasets::publish(manifest, name, copy_mock).await?;
            }
            DatasetCommands::List => {
                commands::datasets::list().await?;
            }
            DatasetCommands::Init {
                name,
                private,
                private_id,
                mock,
                mock_id,
                description,
                author,
                version,
                asset_key,
                asset_type,
                output,
            } => {
                commands::datasets::init(
                    name,
                    private,
                    private_id,
                    mock,
                    mock_id,
                    description,
                    author,
                    version,
                    asset_key,
                    asset_type,
                    output,
                )
                .await?;
            }
        },
        Commands::Config { command, json } => {
            if let Some(cmd) = command {
                match cmd {
                    ConfigCommands::Email { email } => {
                        commands::config_cmd::set_email(email).await?;
                    }
                    ConfigCommands::Syftbox { path } => {
                        commands::config_cmd::set_syftbox(path).await?;
                    }
                }
            } else {
                commands::config_cmd::show(json).await?;
            }
        }
        Commands::Syftbox { command } => match command {
            SyftboxCommands::RequestOtp {
                email,
                name,
                server,
            } => {
                commands::syftbox::request_otp(email, name, server).await?;
            }
            SyftboxCommands::SubmitOtp {
                code,
                email,
                server,
                config,
                data_dir,
                client_url,
            } => {
                commands::syftbox::submit_otp(&code, email, server, config, data_dir, client_url)
                    .await?;
            }
        },
        Commands::Key { command } => {
            let config = biovault::config::Config::load()?;
            commands::key::handle(command, &config).await?;
        }
        Commands::Syc { command } => {
            let config = biovault::config::Config::load()?;
            commands::syc::handle(command, &config).await?;
        }
        Commands::Contacts { command } => {
            let config = biovault::config::Config::load()?;
            commands::contacts::handle(command, &config).await?;
        }
        Commands::Network { command } => {
            let config = biovault::config::Config::load()?;
            commands::network::handle(command, &config).await?;
        }
        Commands::Session { command } => {
            let config = biovault::config::Config::load()?;
            commands::sessions::handle(command, &config).await?;
        }
        Commands::Fastq { command } => match command {
            FastqCommands::Combine {
                input_folder,
                output_file,
                validate,
                no_prompt,
                stats_format,
            } => {
                let should_validate = if no_prompt { Some(validate) } else { None };
                commands::fastq::combine(
                    input_folder,
                    output_file,
                    should_validate,
                    Some(stats_format),
                )
                .await?;
            }
        },
        Commands::Sql { command } => match command {
            SqlCommands::Tables => {
                commands::sql::tables().await?;
            }
            SqlCommands::Structure { table, json } => {
                commands::sql::structure(table, json).await?;
            }
            SqlCommands::Run {
                query,
                output,
                format,
                allow_write,
                allow_ddl,
            } => {
                commands::sql::run(query, output, format, allow_write, allow_ddl).await?;
            }
            SqlCommands::Import {
                file_path,
                table,
                participant_col,
                map,
                format,
                on_mismatch,
                dry_run,
                allow_overwrite,
            } => {
                commands::sql::import(commands::sql::ImportParams {
                    file_path,
                    table,
                    participant_col,
                    map,
                    format,
                    on_mismatch,
                    dry_run,
                    allow_overwrite,
                })
                .await?;
            }
            SqlCommands::Drop { table, confirm } => {
                commands::sql::drop(table, confirm).await?;
            }
        },
        Commands::Files { command } => match command {
            FilesCommands::Scan {
                path,
                ext,
                recursive,
                format,
            } => {
                commands::files::scan(path, ext, recursive, format).await?;
            }
            FilesCommands::Import {
                path,
                ext,
                recursive,
                pattern,
                dry_run,
                non_interactive,
                format,
            } => {
                commands::files::import(
                    path,
                    ext,
                    recursive,
                    pattern,
                    dry_run,
                    non_interactive,
                    format,
                )
                .await?;
            }
            FilesCommands::List {
                ext,
                participant,
                unassigned,
                limit,
                format,
            } => {
                commands::files::list(ext, participant, unassigned, limit, format).await?;
            }
            FilesCommands::Info { file, format } => {
                commands::files::info(file, format).await?;
            }
            FilesCommands::SuggestPatterns {
                path,
                ext,
                recursive,
                format,
            } => {
                commands::files::suggest_patterns(path, ext, recursive, format).await?;
            }
            FilesCommands::Delete { id, format } => {
                commands::files::delete(id, format).await?;
            }
            FilesCommands::DeleteBulk { ids, format } => {
                commands::files::delete_bulk(ids, format).await?;
            }
            FilesCommands::Detect { files, format } => {
                commands::files::detect(files, format).await?;
            }
            FilesCommands::Analyze { files, format } => {
                commands::files::analyze(files, format).await?;
            }
            FilesCommands::Link {
                file_id,
                participant,
                format,
            } => {
                commands::files::link(file_id, participant, format).await?;
            }
            FilesCommands::LinkBulk { json, format } => {
                commands::files::link_bulk(json, format).await?;
            }
            FilesCommands::Unlink { file_id, format } => {
                commands::files::unlink(file_id, format).await?;
            }
            FilesCommands::Hash { files, format } => {
                commands::files::hash(files, format).await?;
            }
            FilesCommands::ExportCsv {
                path,
                ext,
                recursive,
                pattern,
                output,
            } => {
                commands::files::export_csv(path, ext, recursive, pattern, output).await?;
            }
            FilesCommands::DetectCsv { input_csv, output } => {
                commands::files::detect_csv(input_csv, output).await?;
            }
            FilesCommands::AnalyzeCsv { input_csv, output } => {
                commands::files::analyze_csv(input_csv, output).await?;
            }
            FilesCommands::ImportCsv {
                csv_path,
                non_interactive,
                format,
                save_skipped,
            } => {
                commands::files::import_csv(csv_path, non_interactive, format, save_skipped)
                    .await?;
            }
            FilesCommands::ProcessQueue {
                limit,
                daemon,
                format,
            } => {
                commands::files::process_queue(limit, daemon, format).await?;
            }
            FilesCommands::ImportPending { csv_path, format } => {
                commands::files::import_pending(csv_path, format).await?;
            }
        },
        Commands::Submit {
            project_path,
            destination,
            non_interactive,
            force,
        } => {
            commands::submit::submit(project_path, destination, non_interactive, force).await?;
        }
        Commands::Cleanup { all } => {
            let config = biovault::config::Config::load()?;
            commands::messages::cleanup_locks(&config, all)?;
        }
        Commands::Inbox {
            interactive,
            plain,
            json,
            sent,
            all,
            unread,
            projects,
            message_type,
            from,
            search,
        } => {
            let config = biovault::config::Config::load()?;
            // Default behavior: interactive unless --plain is provided
            if plain && !interactive {
                let filters = commands::inbox::ListFilters {
                    sent,
                    all,
                    unread,
                    projects,
                    message_type,
                    from,
                    search,
                    json,
                };
                commands::inbox::list(&config, filters)?;
            } else {
                // When both flags are provided, prefer interactive
                commands::inbox::interactive(&config, None).await?;
            }
        }
        Commands::Message { command } => match command {
            MessageCommands::Send {
                recipient,
                message,
                subject,
            } => {
                let config = biovault::config::Config::load()?;
                commands::messages::send_message(
                    &config,
                    &recipient,
                    &message,
                    subject.as_deref(),
                )?;
            }
            MessageCommands::Reply { message_id, body } => {
                let config = biovault::config::Config::load()?;
                commands::messages::reply_message(&config, &message_id, &body)?;
            }
            MessageCommands::Read {
                message_id,
                non_interactive,
            } => {
                let config = biovault::config::Config::load()?;
                commands::messages::read_message(&config, &message_id, non_interactive).await?;
            }
            MessageCommands::Delete { message_id } => {
                let config = biovault::config::Config::load()?;
                commands::messages::delete_message(&config, &message_id)?;
            }
            MessageCommands::List {
                unread,
                sent,
                projects,
                json,
            } => {
                let config = biovault::config::Config::load()?;
                commands::messages::list_messages(&config, unread, sent, projects, json)?;
            }
            MessageCommands::Thread { thread_id } => {
                let config = biovault::config::Config::load()?;
                commands::messages::view_thread(&config, &thread_id)?;
            }
            MessageCommands::Sync { no_cleanup } => {
                let config = biovault::config::Config::load()?;
                commands::messages::sync_messages(&config, no_cleanup)?;
            }
            MessageCommands::Process {
                message_id,
                test,
                real,
                participant,
                approve,
                non_interactive,
            } => {
                let config = biovault::config::Config::load()?;
                commands::messages::process_project_message(
                    &config,
                    &message_id,
                    test,
                    real,
                    participant,
                    approve,
                    non_interactive,
                )
                .await?;
            }
            MessageCommands::Archive { message_id } => {
                let config = biovault::config::Config::load()?;
                commands::messages::archive_message(&config, &message_id)?;
            }
        },
        Commands::Samplesheet { command } => match command {
            SamplesheetCommands::Create {
                input_dir,
                output_file,
                file_filter,
                extract_cols,
                ignore,
            } => {
                commands::samplesheet::create(
                    input_dir,
                    output_file,
                    file_filter,
                    extract_cols,
                    ignore,
                )
                .await?;
            }
            SamplesheetCommands::ExportCatalog {
                output_file,
                data_type,
                participants,
                path_column,
            } => {
                commands::samplesheet::export_catalog(
                    output_file,
                    data_type,
                    participants,
                    path_column,
                )
                .await?;
            }
        },
        Commands::Daemon { command } => {
            // If BV_DAEMON_CONFIG env var is set, use it (for spawned daemon processes)
            // Otherwise load config normally
            let config = if let Ok(config_json) = std::env::var("BV_DAEMON_CONFIG") {
                serde_json::from_str(&config_json)?
            } else {
                biovault::config::Config::load()?
            };

            match command {
                DaemonCommands::Start { foreground } => {
                    commands::daemon::start(&config, foreground).await?;
                }
                DaemonCommands::Stop => {
                    commands::daemon::stop(&config).await?;
                }
                DaemonCommands::Restart { foreground } => {
                    // Stop the daemon if it's running
                    let _ = commands::daemon::stop(&config).await;
                    // Small delay to ensure clean shutdown
                    tokio::time::sleep(std::time::Duration::from_secs(1)).await;
                    // Start it again
                    commands::daemon::start(&config, foreground).await?;
                }
                DaemonCommands::Status => {
                    commands::daemon::service_status(&config).await?;
                }
                DaemonCommands::Logs { follow, lines } => {
                    commands::daemon::logs(&config, follow, lines).await?;
                }
                DaemonCommands::Install => {
                    commands::daemon::install_service(&config).await?;
                }
                DaemonCommands::Uninstall => {
                    commands::daemon::uninstall_service(&config).await?;
                }
                DaemonCommands::List => {
                    commands::daemon::list_services().await?;
                }
                DaemonCommands::Show => {
                    commands::daemon::show_service(&config).await?;
                }
                DaemonCommands::Reinstall => {
                    commands::daemon::reinstall_service(&config).await?;
                }
            }
        }
        Commands::Python { command } => match command {
            PythonCommands::Install { version } => {
                commands::python::install(&version).await?;
            }
            PythonCommands::List { installed } => {
                commands::python::list(installed).await?;
            }
            PythonCommands::Show => {
                commands::python::show().await?;
            }
            PythonCommands::Uninstall { version } => {
                commands::python::uninstall(&version).await?;
            }
        },
        Commands::Jupyter { command } => match command {
            JupyterCommands::Start {
                project_path,
                python,
            } => {
                commands::jupyter::start(&project_path, &python).await?;
            }
            JupyterCommands::Stop { project_path } => {
                commands::jupyter::stop(&project_path).await?;
            }
            JupyterCommands::Reset {
                project_path,
                python,
            } => {
                commands::jupyter::reset(&project_path, &python).await?;
            }
            JupyterCommands::Status => {
                commands::jupyter::status().await?;
            }
            JupyterCommands::List => {
                commands::jupyter::list().await?;
            }
        },
        Commands::HardReset { ignore_warning } => {
            commands::hard_reset::execute(ignore_warning).await?;
        }
        Commands::Desktop { config } => {
            let biovault_home = if let Some(cfg) = config {
                cfg
            } else if let Ok(env_home) = std::env::var("BIOVAULT_HOME") {
                env_home
            } else {
                biovault::config::get_biovault_home()?
                    .to_string_lossy()
                    .to_string()
            };

            #[cfg(target_os = "macos")]
            {
                let app_path = std::path::Path::new("/Applications/BioVault Desktop.app");
                if !app_path.exists() {
                    eprintln!("Error: BioVault Desktop is not installed.");
                    eprintln!("\nPlease install BioVault Desktop from:");
                    eprintln!("https://github.com/OpenMined/biovault-desktop");
                    eprintln!("\nOr download the latest release:");
                    eprintln!("https://github.com/OpenMined/biovault-desktop/releases/latest");
                    std::process::exit(1);
                }

                match std::process::Command::new("open")
                    .arg("-n")
                    .arg("/Applications/BioVault Desktop.app")
                    .arg("--args")
                    .arg("--biovault-config")
                    .arg(&biovault_home)
                    .spawn()
                {
                    Ok(_) => println!("Launching BioVault Desktop with config: {}", biovault_home),
                    Err(e) => {
                        eprintln!("Error launching BioVault Desktop: {}", e);
                        eprintln!("\nPlease ensure BioVault Desktop is installed from:");
                        eprintln!("https://github.com/OpenMined/biovault-desktop");
                        std::process::exit(1);
                    }
                }
            }

            #[cfg(not(target_os = "macos"))]
            {
                match which::which("bv-desktop").or_else(|_| which::which("biovault-desktop")) {
                    Ok(desktop_bin) => {
                        match std::process::Command::new(desktop_bin)
                            .arg("--biovault-config")
                            .arg(&biovault_home)
                            .spawn()
                        {
                            Ok(_) => println!(
                                "Launching BioVault Desktop with config: {}",
                                biovault_home
                            ),
                            Err(e) => {
                                eprintln!("Error launching BioVault Desktop: {}", e);
                                eprintln!("\nPlease ensure BioVault Desktop is installed from:");
                                eprintln!("https://github.com/OpenMined/biovault-desktop");
                                std::process::exit(1);
                            }
                        }
                    }
                    Err(_) => {
                        eprintln!("Error: BioVault Desktop is not installed or not in PATH.");
                        eprintln!("\nPlease install BioVault Desktop from:");
                        eprintln!("https://github.com/OpenMined/biovault-desktop");
                        eprintln!("\nOr download the latest release:");
                        eprintln!("https://github.com/OpenMined/biovault-desktop/releases/latest");
                        std::process::exit(1);
                    }
                }
            }
        }
    }

    Ok(())
}

async fn async_main() -> Result<()> {
    let cli = Cli::parse();
    async_main_with(cli).await
}
