//! quicproquo CLI client.

// ── v2 feature gate: when compiled with --features v2, use the SDK-based CLI.
#[cfg(feature = "v2")]
mod v2_commands;
#[cfg(feature = "v2")]
mod v2_main;

#[cfg(feature = "v2")]
fn main() {
    v2_main::main();
}

// ── v1 CLI (default) ─────────────────────────────────────────────────────────
#[cfg(not(feature = "v2"))]
use std::path::{Path, PathBuf};
#[cfg(not(feature = "v2"))]
use anyhow::Context;
#[cfg(not(feature = "v2"))]
use clap::{Parser, Subcommand};
#[cfg(not(feature = "v2"))]
use quicproquo_client::{
    cmd_chat, cmd_check_key, cmd_create_group, cmd_demo_group, cmd_export, cmd_export_verify,
    cmd_fetch_key, cmd_health, cmd_invite, cmd_join, cmd_login, cmd_ping, cmd_recv, cmd_register,
    cmd_register_state, cmd_refresh_keypackage, cmd_register_user, cmd_send, cmd_whoami,
    init_auth, run_repl, set_insecure_skip_verify, ClientAuth,
};
#[cfg(all(feature = "tui", not(feature = "v2")))]
use quicproquo_client::client::tui::run_tui;

// ── CLI ───────────────────────────────────────────────────────────────────────
#[cfg(not(feature = "v2"))]

#[derive(Debug, Parser)]
#[command(name = "qpq", about = "quicproquo CLI client", version)]
struct Args {
    /// Path to the server's TLS certificate (self-signed by default).
    #[arg(
        long,
        global = true,
        default_value = "data/server-cert.der",
        env = "QPQ_CA_CERT"
    )]
    ca_cert: PathBuf,

    /// Expected TLS server name (must match the certificate SAN).
    #[arg(
        long,
        global = true,
        default_value = "localhost",
        env = "QPQ_SERVER_NAME"
    )]
    server_name: String,

    /// Bearer token or OPAQUE session token for authenticated requests.
    /// Not required for register-user and login commands.
    #[arg(
        long,
        global = true,
        env = "QPQ_ACCESS_TOKEN",
        default_value = ""
    )]
    access_token: String,

    /// Optional device identifier (UUID bytes encoded as hex or raw string).
    #[arg(long, global = true, env = "QPQ_DEVICE_ID")]
    device_id: Option<String>,

    /// Password to encrypt/decrypt client state files (QPCE format).
    /// If set, state files are encrypted at rest with Argon2id + ChaCha20Poly1305.
    #[arg(long, global = true, env = "QPQ_STATE_PASSWORD")]
    state_password: Option<String>,

    /// DANGER: Skip TLS certificate verification. Development only.
    /// Disables all certificate checks, making the connection vulnerable to MITM attacks.
    #[arg(
        long = "danger-accept-invalid-certs",
        global = true,
        env = "QPQ_DANGER_ACCEPT_INVALID_CERTS"
    )]
    danger_accept_invalid_certs: bool,

    // ── Default-repl args (used when no subcommand is given) ─────────
    /// State file path (identity + MLS state). Used when running the default REPL.
    #[arg(long, default_value = "qpq-state.bin", env = "QPQ_STATE")]
    state: PathBuf,

    /// Server address (host:port). Used when running the default REPL.
    #[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")]
    server: String,

    /// OPAQUE username for automatic registration/login.
    #[arg(long, env = "QPQ_USERNAME")]
    username: Option<String>,

    /// OPAQUE password (prompted securely if --username is set but --password is not).
    #[arg(long, env = "QPQ_PASSWORD")]
    password: Option<String>,

    /// Do not auto-start a local qpq-server (useful when connecting to a remote server).
    #[arg(long, env = "QPQ_NO_SERVER")]
    no_server: bool,

    #[command(subcommand)]
    command: Option<Command>,
}

#[cfg(not(feature = "v2"))]
#[derive(Debug, Subcommand)]
enum Command {
    /// Register a new user via OPAQUE (password never leaves the client).
    RegisterUser {
        #[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")]
        server: String,
        /// Username for the new account.
        #[arg(long)]
        username: String,
        /// Password (will be used in OPAQUE PAKE; server never sees it).
        #[arg(long)]
        password: String,
    },

    /// Log in via OPAQUE and receive a session token.
    Login {
        #[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")]
        server: String,
        #[arg(long)]
        username: String,
        #[arg(long)]
        password: String,
        /// Hex-encoded Ed25519 identity key (64 hex chars). Optional if --state is provided.
        #[arg(long)]
        identity_key: Option<String>,
        /// State file to derive the identity key (requires same password if encrypted).
        #[arg(long)]
        state: Option<PathBuf>,
        /// Password for the encrypted state file (if any).
        #[arg(long)]
        state_password: Option<String>,
    },

    /// Show local identity key, fingerprint, group status, and hybrid key status.
    Whoami {
        /// State file path (identity + MLS state).
        #[arg(
            long,
            default_value = "qpq-state.bin",
            env = "QPQ_STATE"
        )]
        state: PathBuf,
    },

    /// Check server connectivity and print status.
    Health {
        /// Server address (host:port).
        #[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")]
        server: String,
    },

    /// Check if a peer has registered a hybrid key (non-consuming lookup).
    CheckKey {
        /// Server address (host:port).
        #[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")]
        server: String,

        /// Peer's Ed25519 identity public key (64 hex chars = 32 bytes).
        identity_key: String,
    },

    /// Send a Ping to the server and print the round-trip time.
    Ping {
        /// Server address (host:port).
        #[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")]
        server: String,
    },

    /// Generate a fresh MLS KeyPackage and upload it to the Authentication Service.
    Register {
        /// Server address (host:port).
        #[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")]
        server: String,
    },

    /// Fetch a peer's KeyPackage from the Authentication Service.
    FetchKey {
        /// Server address (host:port).
        #[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")]
        server: String,

        /// Target peer's Ed25519 identity public key (64 hex chars = 32 bytes).
        identity_key: String,
    },

    /// Run a two-party MLS demo (creator + joiner) against live AS and DS.
    DemoGroup {
        /// Server address (host:port).
        #[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")]
        server: String,
    },

    /// Upload the persistent identity's KeyPackage to the AS (uses state file).
    RegisterState {
        /// State file path (identity + MLS state).
        #[arg(
            long,
            default_value = "qpq-state.bin",
            env = "QPQ_STATE"
        )]
        state: PathBuf,

        /// Authentication Service address (host:port).
        #[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")]
        server: String,
    },

    /// Refresh the KeyPackage on the server (existing state only).
    /// Run periodically (e.g. before server TTL ~24h) or after your KeyPackage was consumed so others can invite you.
    RefreshKeypackage {
        /// State file path (identity + MLS state).
        #[arg(
            long,
            default_value = "qpq-state.bin",
            env = "QPQ_STATE"
        )]
        state: PathBuf,

        /// Server address (host:port).
        #[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")]
        server: String,
    },

    /// Create a persistent group and save state to disk.
    CreateGroup {
        /// State file path (identity + MLS state).
        #[arg(
            long,
            default_value = "qpq-state.bin",
            env = "QPQ_STATE"
        )]
        state: PathBuf,

        /// Server address (host:port).
        #[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")]
        server: String,

        /// Group identifier (arbitrary bytes, typically a human-readable name).
        #[arg(long)]
        group_id: String,
    },

    /// Invite a peer into the group and deliver a Welcome via DS.
    Invite {
        #[arg(
            long,
            default_value = "qpq-state.bin",
            env = "QPQ_STATE"
        )]
        state: PathBuf,
        #[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")]
        server: String,
        /// Peer identity public key (64 hex chars = 32 bytes).
        #[arg(long)]
        peer_key: String,
    },

    /// Join a group by fetching the Welcome from the DS.
    Join {
        #[arg(
            long,
            default_value = "qpq-state.bin",
            env = "QPQ_STATE"
        )]
        state: PathBuf,
        #[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")]
        server: String,
    },

    /// Send an application message via the DS.
    Send {
        #[arg(
            long,
            default_value = "qpq-state.bin",
            env = "QPQ_STATE"
        )]
        state: PathBuf,
        #[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")]
        server: String,
        /// Recipient identity key (hex, 32 bytes -> 64 chars). Omit when using --all.
        #[arg(long)]
        peer_key: Option<String>,
        /// Send to all other group members (N-way groups).
        #[arg(long)]
        all: bool,
        /// Plaintext message to send.
        #[arg(long)]
        msg: String,
    },

    /// Receive and decrypt all pending messages from the DS.
    Recv {
        #[arg(
            long,
            default_value = "qpq-state.bin",
            env = "QPQ_STATE"
        )]
        state: PathBuf,
        #[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")]
        server: String,

        /// Wait for up to this many milliseconds if no messages are queued.
        #[arg(long, default_value_t = 0)]
        wait_ms: u64,

        /// Continuously long-poll for messages.
        #[arg(long)]
        stream: bool,
    },

    /// Interactive multi-conversation REPL. Supports /dm, /create-group, /invite, /join, /switch, and more.
    /// Automatically registers and logs in if --username/--password are provided (or prompts interactively).
    Repl {
        #[arg(
            long,
            default_value = "qpq-state.bin",
            env = "QPQ_STATE"
        )]
        state: PathBuf,
        #[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")]
        server: String,
        /// OPAQUE username for automatic registration/login.
        #[arg(long, env = "QPQ_USERNAME")]
        username: Option<String>,
        /// OPAQUE password (prompted securely if --username is set but --password is not).
        #[arg(long, env = "QPQ_PASSWORD")]
        password: Option<String>,
        /// Do not auto-start a local qpq-server.
        #[arg(long, env = "QPQ_NO_SERVER")]
        no_server: bool,
    },

    /// Full-screen Ratatui TUI (requires --features tui).
    /// Channels sidebar, scrollable message view, and inline input bar.
    #[cfg(feature = "tui")]
    Tui {
        #[arg(
            long,
            default_value = "qpq-state.bin",
            env = "QPQ_STATE"
        )]
        state: PathBuf,
        #[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")]
        server: String,
        /// OPAQUE username for automatic registration/login.
        #[arg(long, env = "QPQ_USERNAME")]
        username: Option<String>,
        /// OPAQUE password (prompted securely if --username is set but --password is not).
        #[arg(long, env = "QPQ_PASSWORD")]
        password: Option<String>,
    },

    /// Interactive 1:1 chat: type to send, incoming messages printed as [peer] <msg>. Ctrl+D to exit.
    /// In a two-person group, peer is chosen automatically; use --peer-key only with 3+ members.
    Chat {
        #[arg(
            long,
            default_value = "qpq-state.bin",
            env = "QPQ_STATE"
        )]
        state: PathBuf,
        #[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")]
        server: String,
        /// Peer identity key (hex, 64 chars). Omit in a two-person group to use the only other member.
        #[arg(long)]
        peer_key: Option<String>,
        /// How often to poll for incoming messages (milliseconds).
        #[arg(long, default_value_t = 500)]
        poll_interval_ms: u64,
    },

    /// Export a conversation's message history to an encrypted, tamper-evident transcript file.
    ///
    /// The output file uses Argon2id + ChaCha20-Poly1305 encryption with a SHA-256 hash chain
    /// linking every record. Use `qpq export verify` to check chain integrity without decrypting.
    Export {
        /// Path to the conversation database (.convdb file).
        #[arg(long, default_value = "qpq-convdb.sqlite", env = "QPQ_CONV_DB")]
        conv_db: PathBuf,

        /// Conversation ID to export (32 hex chars = 16 bytes).
        #[arg(long)]
        conv_id: String,

        /// Output path for the .qpqt transcript file.
        #[arg(long, default_value = "transcript.qpqt")]
        output: PathBuf,

        /// Password used to encrypt the transcript (separate from the state/DB password).
        #[arg(long, env = "QPQ_TRANSCRIPT_PASSWORD")]
        transcript_password: Option<String>,

        /// Password for the encrypted conversation database (if any).
        #[arg(long, env = "QPQ_STATE_PASSWORD")]
        db_password: Option<String>,
    },

    /// Verify the hash-chain integrity of a transcript file without decrypting content.
    ExportVerify {
        /// Path to the .qpqt transcript file to verify.
        #[arg(long)]
        input: PathBuf,
    },

    /// Execute a YAML playbook (scripted command sequence) and exit.
    /// Requires `--features playbook`.
    #[cfg(feature = "playbook")]
    Run {
        /// Path to the YAML playbook file.
        playbook: PathBuf,

        /// State file path (identity + MLS state).
        #[arg(long, default_value = "qpq-state.bin", env = "QPQ_STATE")]
        state: PathBuf,

        /// Server address (host:port).
        #[arg(long, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")]
        server: String,

        /// OPAQUE username for automatic login.
        #[arg(long, env = "QPQ_USERNAME")]
        username: Option<String>,

        /// OPAQUE password.
        #[arg(long, env = "QPQ_PASSWORD")]
        password: Option<String>,

        /// Override playbook variables: KEY=VALUE (repeatable).
        #[arg(long = "var", short = 'V')]
        vars: Vec<String>,
    },
}

// ── Helpers ───────────────────────────────────────────────────────────────────
#[cfg(not(feature = "v2"))]
/// Returns `qpq-{username}.bin` when `state` is still at the default
/// (`qpq-state.bin`) and a username has been provided. Otherwise returns
/// `state` unchanged. This lets `qpq --username alice` automatically isolate
/// Alice's state without requiring a manual `--state` flag.
fn derive_state_path(state: PathBuf, username: Option<&str>) -> PathBuf {
    if state == Path::new("qpq-state.bin") {
        if let Some(uname) = username {
            return PathBuf::from(format!("qpq-{uname}.bin"));
        }
    }
    state
}

// ── Playbook execution ───────────────────────────────────────────────────────

#[cfg(all(feature = "playbook", not(feature = "v2")))]
async fn run_playbook(
    playbook_path: &Path,
    state: &Path,
    server: &str,
    ca_cert: &Path,
    server_name: &str,
    state_pw: Option<&str>,
    username: Option<&str>,
    password: Option<&str>,
    access_token: &str,
    device_id: Option<&str>,
    extra_vars: &[String],
) -> anyhow::Result<()> {
    use quicproquo_client::PlaybookRunner;

    let insecure = std::env::var("QPQ_DANGER_ACCEPT_INVALID_CERTS").is_ok();

    // Connect to server.
    let client =
        quicproquo_client::connect_node_opt(server, ca_cert, server_name, insecure)
            .await
            .context("connect to server")?;

    // Build session state.
    let mut session = quicproquo_client::client::session::SessionState::load(state, state_pw)
        .context("load session state")?;

    // If username/password provided, do OPAQUE login.
    if let (Some(uname), Some(pw)) = (username, password) {
        if let Err(e) =
            quicproquo_client::opaque_login(&client, uname, pw, &session.identity.public_key_bytes()).await
        {
            eprintln!("OPAQUE login failed: {e:#}");
        }
    } else if !access_token.is_empty() {
        let auth = ClientAuth::from_parts(access_token.to_string(), device_id.map(String::from));
        init_auth(auth);
    }

    // Load playbook.
    let mut runner = PlaybookRunner::from_file(playbook_path)
        .with_context(|| format!("load playbook: {}", playbook_path.display()))?;

    // Inject extra variables from --var KEY=VALUE flags.
    for kv in extra_vars {
        if let Some((k, v)) = kv.split_once('=') {
            runner.set_var(k, v);
        } else {
            eprintln!("warning: ignoring malformed --var '{kv}' (expected KEY=VALUE)");
        }
    }

    // Inject connection info as variables.
    runner.set_var("_server", server);
    if let Some(u) = username {
        runner.set_var("_username", u);
    }

    let report = runner.run(&mut session, &client).await;
    print!("{report}");

    if report.all_passed() {
        Ok(())
    } else {
        std::process::exit(1);
    }
}

// ── Entry point ───────────────────────────────────────────────────────────────

#[cfg(not(feature = "v2"))]
#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Install the rustls crypto provider before any TLS operations.
    let _ = rustls::crypto::ring::default_provider().install_default();

    tracing_subscriber::fmt()
        .with_env_filter(
            tracing_subscriber::EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn")),
        )
        .init();

    let args = Args::parse();

    if args.danger_accept_invalid_certs {
        eprintln!("WARNING: TLS verification disabled — insecure mode");
        set_insecure_skip_verify(true);
    }

    // For the REPL and TUI, defer init_auth so they can resolve their own token via OPAQUE.
    // For all other subcommands, initialize auth immediately.
    #[cfg(not(feature = "tui"))]
    let is_repl = matches!(args.command, None | Some(Command::Repl { .. }));
    #[cfg(feature = "tui")]
    let is_repl = matches!(args.command, None | Some(Command::Repl { .. }) | Some(Command::Tui { .. }));
    if !is_repl {
        let auth_ctx = ClientAuth::from_parts(args.access_token.clone(), args.device_id.clone());
        init_auth(auth_ctx);
    }

    let state_pw = args.state_password.as_deref();

    // Default to REPL when no subcommand is given.
    let no_server = args.no_server;
    let command = args.command.unwrap_or_else(|| Command::Repl {
        state: derive_state_path(args.state, args.username.as_deref()),
        server: args.server,
        username: args.username,
        password: args.password,
        no_server,
    });

    match command {
        Command::RegisterUser {
            server,
            username,
            password,
        } => {
            let local = tokio::task::LocalSet::new();
            local
                .run_until(cmd_register_user(
                    &server,
                    &args.ca_cert,
                    &args.server_name,
                    &username,
                    &password,
                    None,
                ))
                .await
        }
        Command::Login {
            server,
            username,
            password,
            identity_key,
            state,
            state_password,
        } => {
            let local = tokio::task::LocalSet::new();
            local
                .run_until(cmd_login(
                    &server,
                    &args.ca_cert,
                    &args.server_name,
                    &username,
                    &password,
                    identity_key.as_deref(),
                    state.as_deref(),
                    state_password.as_deref(),
                ))
                .await
        }
        Command::Whoami { state } => cmd_whoami(&state, state_pw),
        Command::Health { server } => {
            let local = tokio::task::LocalSet::new();
            local
                .run_until(cmd_health(&server, &args.ca_cert, &args.server_name))
                .await
        }
        Command::CheckKey {
            server,
            identity_key,
        } => {
            let local = tokio::task::LocalSet::new();
            local
                .run_until(cmd_check_key(
                    &server,
                    &args.ca_cert,
                    &args.server_name,
                    &identity_key,
                ))
                .await
        }
        Command::Ping { server } => cmd_ping(&server, &args.ca_cert, &args.server_name).await,
        Command::Register { server } => {
            let local = tokio::task::LocalSet::new();
            local
                .run_until(cmd_register(&server, &args.ca_cert, &args.server_name))
                .await
        }
        Command::FetchKey {
            server,
            identity_key,
        } => {
            let local = tokio::task::LocalSet::new();
            local
                .run_until(cmd_fetch_key(
                    &server,
                    &args.ca_cert,
                    &args.server_name,
                    &identity_key,
                ))
                .await
        }
        Command::DemoGroup { server } => {
            let local = tokio::task::LocalSet::new();
            local
                .run_until(cmd_demo_group(&server, &args.ca_cert, &args.server_name))
                .await
        }
        Command::RegisterState { state, server } => {
            let local = tokio::task::LocalSet::new();
            local
                .run_until(cmd_register_state(
                    &state,
                    &server,
                    &args.ca_cert,
                    &args.server_name,
                    state_pw,
                ))
                .await
        }
        Command::RefreshKeypackage { state, server } => {
            let local = tokio::task::LocalSet::new();
            local
                .run_until(cmd_refresh_keypackage(
                    &state,
                    &server,
                    &args.ca_cert,
                    &args.server_name,
                    state_pw,
                ))
                .await
        }
        Command::CreateGroup {
            state,
            server,
            group_id,
        } => {
            let local = tokio::task::LocalSet::new();
            local
                .run_until(cmd_create_group(&state, &server, &group_id, state_pw))
                .await
        }
        Command::Invite {
            state,
            server,
            peer_key,
        } => {
            let local = tokio::task::LocalSet::new();
            local
                .run_until(cmd_invite(
                    &state,
                    &server,
                    &args.ca_cert,
                    &args.server_name,
                    &peer_key,
                    state_pw,
                ))
                .await
        }
        Command::Join { state, server } => {
            let local = tokio::task::LocalSet::new();
            local
                .run_until(cmd_join(
                    &state,
                    &server,
                    &args.ca_cert,
                    &args.server_name,
                    state_pw,
                ))
                .await
        }
        Command::Send {
            state,
            server,
            peer_key,
            all,
            msg,
        } => {
            let local = tokio::task::LocalSet::new();
            local
                .run_until(cmd_send(
                    &state,
                    &server,
                    &args.ca_cert,
                    &args.server_name,
                    peer_key.as_deref(),
                    all,
                    &msg,
                    state_pw,
                ))
                .await
        }
        Command::Recv {
            state,
            server,
            wait_ms,
            stream,
        } => {
            let local = tokio::task::LocalSet::new();
            local
                .run_until(cmd_recv(
                    &state,
                    &server,
                    &args.ca_cert,
                    &args.server_name,
                    wait_ms,
                    stream,
                    state_pw,
                ))
                .await
        }
        Command::Repl {
            state,
            server,
            username,
            password,
            no_server,
        } => {
            let state = derive_state_path(state, username.as_deref());
            let local = tokio::task::LocalSet::new();
            local
                .run_until(run_repl(
                    &state,
                    &server,
                    &args.ca_cert,
                    &args.server_name,
                    state_pw,
                    username.as_deref(),
                    password.as_deref(),
                    &args.access_token,
                    args.device_id.as_deref(),
                    no_server,
                ))
                .await
        }
        Command::Chat {
            state,
            server,
            peer_key,
            poll_interval_ms,
        } => {
            let local = tokio::task::LocalSet::new();
            local
                .run_until(cmd_chat(
                    &state,
                    &server,
                    &args.ca_cert,
                    &args.server_name,
                    peer_key.as_deref(),
                    state_pw,
                    poll_interval_ms,
                ))
                .await
        }
        #[cfg(feature = "tui")]
        Command::Tui {
            state,
            server,
            username,
            password,
        } => {
            let state = derive_state_path(state, username.as_deref());
            let local = tokio::task::LocalSet::new();
            local
                .run_until(run_tui(
                    &state,
                    &server,
                    &args.ca_cert,
                    &args.server_name,
                    state_pw,
                    username.as_deref(),
                    password.as_deref(),
                    &args.access_token,
                    args.device_id.as_deref(),
                ))
                .await
        }
        Command::Export {
            conv_db,
            conv_id,
            output,
            transcript_password,
            db_password,
        } => {
            // Prompt for transcript password if not provided.
            let tp = match transcript_password {
                Some(p) => p,
                None => {
                    eprint!("Transcript password: ");
                    rpassword::read_password()
                        .context("failed to read transcript password")?
                }
            };
            cmd_export(
                &conv_db,
                &conv_id,
                &output,
                &tp,
                db_password.as_deref().or(state_pw),
            )
        }
        Command::ExportVerify { input } => cmd_export_verify(&input),
        #[cfg(feature = "playbook")]
        Command::Run {
            playbook,
            state,
            server,
            username,
            password,
            vars,
        } => {
            let state = derive_state_path(state, username.as_deref());
            let local = tokio::task::LocalSet::new();
            local
                .run_until(run_playbook(
                    &playbook,
                    &state,
                    &server,
                    &args.ca_cert,
                    &args.server_name,
                    state_pw,
                    username.as_deref(),
                    password.as_deref(),
                    &args.access_token,
                    args.device_id.as_deref(),
                    &vars,
                ))
                .await
        }
    }
}
