//! v2 CLI entry point — thin shell over `quicproquo_sdk::QpqClient`.
//!
//! Activated via `--features v2`. Replaces the v1 Cap'n Proto RPC main
//! with a simplified command surface backed by the SDK.

use std::path::{Path, PathBuf};
use std::process::Command as ProcessCommand;
use std::time::Duration;

use anyhow::Context;
use clap::{Parser, Subcommand};

use quicproquo_sdk::client::QpqClient;
use quicproquo_sdk::config::ClientConfig;

use crate::v2_commands;

// ── CLI ───────────────────────────────────────────────────────────────────────

#[derive(Debug, Parser)]
#[command(name = "qpq", about = "quicproquo CLI client (v2)", version)]
struct Args {
    /// Server address (host:port).
    #[arg(long, global = true, default_value = "127.0.0.1:7000", env = "QPQ_SERVER")]
    server: String,

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

    /// Path to local conversation database.
    #[arg(long, global = true, default_value = "conversations.db", env = "QPQ_CONV_DB")]
    db_path: PathBuf,

    /// Password for encrypting the local database.
    #[arg(long, global = true, env = "QPQ_DB_PASSWORD")]
    db_password: Option<String>,

    /// Path to the client state file (identity key, MLS state).
    #[arg(long, global = true, default_value = "qpq-state.bin", env = "QPQ_STATE")]
    state: PathBuf,

    /// DANGER: Skip TLS certificate verification. Development only.
    #[arg(
        long = "danger-accept-invalid-certs",
        global = true,
        env = "QPQ_DANGER_ACCEPT_INVALID_CERTS"
    )]
    danger_accept_invalid_certs: bool,

    /// Do not auto-start a local qpq-server.
    #[arg(long, global = true, env = "QPQ_NO_SERVER")]
    no_server: bool,

    #[command(subcommand)]
    command: Cmd,
}

#[derive(Debug, Subcommand)]
enum Cmd {
    /// Register a new user via OPAQUE (password never leaves the client).
    RegisterUser {
        /// Username for the new account.
        #[arg(long)]
        username: String,
        /// Password (used in OPAQUE PAKE; server never sees it).
        #[arg(long)]
        password: String,
    },

    /// Log in via OPAQUE and receive a session token.
    Login {
        #[arg(long)]
        username: String,
        #[arg(long)]
        password: String,
    },

    /// Show local identity info.
    Whoami,

    /// Server health check.
    Health,

    /// Send a message to a conversation.
    Send {
        /// Conversation name (group name or DM peer username).
        #[arg(long)]
        to: String,
        /// Message text.
        #[arg(long)]
        msg: String,
    },

    /// Receive pending messages from a conversation.
    Recv {
        /// Conversation name.
        #[arg(long)]
        from: String,
    },

    /// Start or resume a DM with a user.
    Dm {
        /// Peer username.
        username: String,
    },

    /// Group management commands.
    Group {
        #[command(subcommand)]
        action: GroupCmd,
    },

    /// Resolve a username to its identity key.
    Resolve {
        /// Username to look up.
        username: String,
    },

    /// Device management.
    Devices {
        #[command(subcommand)]
        action: DevicesCmd,
    },

    /// Account recovery management.
    Recovery {
        #[command(subcommand)]
        action: RecoveryCmd,
    },

    /// Offline outbox management.
    Outbox {
        #[command(subcommand)]
        action: OutboxCmd,
    },
}

#[derive(Debug, Subcommand)]
enum GroupCmd {
    /// Create a new group.
    Create {
        /// Group name.
        name: String,
    },
    /// Invite a user to a group.
    Invite {
        /// Group name.
        #[arg(long)]
        group: String,
        /// Username to invite.
        #[arg(long)]
        user: String,
    },
}

#[derive(Debug, Subcommand)]
enum DevicesCmd {
    /// List registered devices.
    List,
    /// Register a new device.
    Register {
        /// Device ID (hex).
        #[arg(long)]
        id: String,
        /// Human-readable device name.
        #[arg(long)]
        name: String,
    },
    /// Revoke a device.
    Revoke {
        /// Device ID (hex).
        #[arg(long)]
        id: String,
    },
}

#[derive(Debug, Subcommand)]
enum RecoveryCmd {
    /// Generate recovery codes and upload encrypted bundles.
    Setup,
    /// Recover account from a recovery code.
    Restore {
        /// Recovery code (e.g. "A3B7K9").
        code: String,
    },
}

#[derive(Debug, Subcommand)]
enum OutboxCmd {
    /// Show pending outbox entries.
    List,
    /// Retry sending all pending outbox entries.
    Retry,
    /// Clear permanently failed outbox entries.
    Clear,
}

// ── Auto-server launch ───────────────────────────────────────────────────────

/// RAII guard that kills an auto-started server process on drop.
struct ServerGuard(Option<std::process::Child>);

impl Drop for ServerGuard {
    fn drop(&mut self) {
        if let Some(ref mut child) = self.0 {
            let _ = child.kill();
            let _ = child.wait();
        }
    }
}

/// Find the `qpq-server` binary: same directory as current exe, then PATH.
fn find_server_binary() -> Option<PathBuf> {
    if let Ok(exe) = std::env::current_exe() {
        let sibling = exe.with_file_name("qpq-server");
        if sibling.exists() {
            return Some(sibling);
        }
    }
    std::env::var_os("PATH").and_then(|paths| {
        std::env::split_paths(&paths)
            .map(|dir| dir.join("qpq-server"))
            .find(|p| p.exists())
    })
}

/// Try a QUIC health probe to the server address.
async fn probe_server(server_addr: &str) -> bool {
    use std::net::ToSocketAddrs;
    let addr = match server_addr.to_socket_addrs() {
        Ok(mut addrs) => match addrs.next() {
            Some(a) => a,
            None => return false,
        },
        Err(_) => return false,
    };
    // Simple TCP probe — if the port is open, the server is likely running.
    tokio::net::TcpStream::connect(addr)
        .await
        .is_ok()
}

/// Start a local qpq-server if one isn't already running.
/// Returns a guard that kills the child on drop (if we started one).
async fn ensure_server_running(
    server_addr: &str,
    data_dir: &Path,
    no_server: bool,
) -> anyhow::Result<ServerGuard> {
    if no_server {
        return Ok(ServerGuard(None));
    }

    if probe_server(server_addr).await {
        return Ok(ServerGuard(None));
    }

    let binary = find_server_binary().ok_or_else(|| {
        anyhow::anyhow!(
            "server at {server_addr} is not reachable and qpq-server binary not found; \
             start a server manually or install qpq-server"
        )
    })?;

    let cert_path = data_dir.join("server-cert.der");
    let key_path = data_dir.join("server-key.der");

    eprintln!("starting server on {server_addr}...");

    let child = ProcessCommand::new(&binary)
        .args([
            "--allow-insecure-auth",
            "--listen",
            server_addr,
            "--tls-cert",
            &cert_path.to_string_lossy(),
            "--tls-key",
            &key_path.to_string_lossy(),
        ])
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::null())
        .spawn()
        .with_context(|| format!("failed to spawn {}", binary.display()))?;

    let guard = ServerGuard(Some(child));

    // Poll until the server is ready.
    let mut delay = Duration::from_millis(100);
    let max_wait = Duration::from_secs(3);
    let start = std::time::Instant::now();

    loop {
        tokio::time::sleep(delay).await;

        if probe_server(server_addr).await {
            eprintln!("server ready");
            return Ok(guard);
        }

        if start.elapsed() > max_wait {
            anyhow::bail!(
                "auto-started qpq-server but it did not become ready within {max_wait:?}"
            );
        }

        delay = (delay * 2).min(Duration::from_secs(1));
    }
}

// ── Helpers ──────────────────────────────────────────────────────────────────

/// Build a `ClientConfig` from CLI args.
fn build_config(args: &Args) -> anyhow::Result<ClientConfig> {
    let server_addr = args
        .server
        .parse()
        .with_context(|| format!("invalid server address: {}", args.server))?;

    Ok(ClientConfig {
        server_addr,
        server_name: args.server_name.clone(),
        db_path: args.db_path.clone(),
        db_password: args.db_password.clone(),
        state_path: args.state.clone(),
        accept_invalid_certs: args.danger_accept_invalid_certs,
        ..ClientConfig::default()
    })
}

/// Build, connect, and return a `QpqClient`. Loads identity from state file
/// if it exists.
async fn connect_client(args: &Args) -> anyhow::Result<QpqClient> {
    let config = build_config(args)?;
    let mut client = QpqClient::new(config);

    // Try loading identity from state file.
    if args.state.exists() {
        match quicproquo_sdk::state::load_state(&args.state, args.db_password.as_deref()) {
            Ok(stored) => {
                let keypair = quicproquo_core::IdentityKeypair::from_seed(stored.identity_seed);
                client.set_identity_key(keypair.public_key_bytes().to_vec());
            }
            Err(e) => {
                tracing::debug!("could not load state from {}: {e}", args.state.display());
            }
        }
    }

    client.connect().await.context("failed to connect to server")?;
    Ok(client)
}

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

pub fn main() {
    // 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();

    let rt = tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .build()
        .unwrap_or_else(|e| {
            eprintln!("fatal: {e}");
            std::process::exit(1);
        });

    if let Err(e) = rt.block_on(run(args)) {
        eprintln!("error: {e:#}");
        std::process::exit(1);
    }
}

async fn run(args: Args) -> anyhow::Result<()> {
    // Auto-start server if needed (except for whoami which is local-only).
    let data_dir = args.state.parent().unwrap_or_else(|| Path::new("."));
    let _server_guard = match args.command {
        Cmd::Whoami => ServerGuard(None),
        _ => ensure_server_running(&args.server, data_dir, args.no_server).await?,
    };

    match args.command {
        Cmd::RegisterUser {
            ref username,
            ref password,
        } => {
            let mut client = connect_client(&args).await?;
            v2_commands::cmd_register_user(&mut client, username, password)
                .await
                .context("register-user failed")?;
        }

        Cmd::Login {
            ref username,
            ref password,
        } => {
            let mut client = connect_client(&args).await?;
            v2_commands::cmd_login(&mut client, username, password)
                .await
                .context("login failed")?;
        }

        Cmd::Whoami => {
            // Whoami is local-only — create client without connecting.
            let config = build_config(&args)?;
            let mut client = QpqClient::new(config);
            if args.state.exists() {
                match quicproquo_sdk::state::load_state(
                    &args.state,
                    args.db_password.as_deref(),
                ) {
                    Ok(stored) => {
                        let keypair =
                            quicproquo_core::IdentityKeypair::from_seed(stored.identity_seed);
                        client.set_identity_key(keypair.public_key_bytes().to_vec());
                    }
                    Err(e) => {
                        eprintln!("warning: could not load state: {e}");
                    }
                }
            }
            v2_commands::cmd_whoami(&client);
        }

        Cmd::Health => {
            let mut client = connect_client(&args).await?;
            v2_commands::cmd_health(&mut client)
                .await
                .context("health check failed")?;
        }

        Cmd::Resolve { ref username } => {
            let mut client = connect_client(&args).await?;
            v2_commands::cmd_resolve(&mut client, username)
                .await
                .context("resolve failed")?;
        }

        Cmd::Dm { ref username } => {
            let mut client = connect_client(&args).await?;
            v2_commands::cmd_resolve(&mut client, username)
                .await
                .context("dm setup failed")?;
            // For now, print the resolved key. Full DM creation requires
            // MLS group state, which will be handled in the REPL flow.
            println!("(DM creation with full MLS setup is available in the REPL)");
        }

        Cmd::Send { ref to, ref msg } => {
            let _ = (to, msg);
            let _client = connect_client(&args).await?;
            // Full send requires MLS group state restoration — deferred to REPL.
            println!("(send is currently available in the REPL; one-shot send coming soon)");
        }

        Cmd::Recv { ref from } => {
            let _ = from;
            let _client = connect_client(&args).await?;
            println!("(recv is currently available in the REPL; one-shot recv coming soon)");
        }

        Cmd::Group {
            action: GroupCmd::Create { ref name },
        } => {
            let _ = name;
            let _client = connect_client(&args).await?;
            println!("(group create is currently available in the REPL; one-shot coming soon)");
        }

        Cmd::Group {
            action:
                GroupCmd::Invite {
                    ref group,
                    ref user,
                },
        } => {
            let _ = (group, user);
            let _client = connect_client(&args).await?;
            println!("(group invite is currently available in the REPL; one-shot coming soon)");
        }

        Cmd::Devices {
            action: DevicesCmd::List,
        } => {
            let mut client = connect_client(&args).await?;
            v2_commands::cmd_devices_list(&mut client)
                .await
                .context("devices list failed")?;
        }

        Cmd::Devices {
            action: DevicesCmd::Register { ref id, ref name },
        } => {
            let mut client = connect_client(&args).await?;
            v2_commands::cmd_devices_register(&mut client, id, name)
                .await
                .context("device register failed")?;
        }

        Cmd::Devices {
            action: DevicesCmd::Revoke { ref id },
        } => {
            let mut client = connect_client(&args).await?;
            v2_commands::cmd_devices_revoke(&mut client, id)
                .await
                .context("device revoke failed")?;
        }

        Cmd::Recovery {
            action: RecoveryCmd::Setup,
        } => {
            let mut client = connect_client(&args).await?;
            v2_commands::cmd_recovery_setup(&mut client)
                .await
                .context("recovery setup failed")?;
        }

        Cmd::Recovery {
            action: RecoveryCmd::Restore { ref code },
        } => {
            let mut client = connect_client(&args).await?;
            v2_commands::cmd_recovery_restore(&mut client, code)
                .await
                .context("recovery restore failed")?;
        }

        Cmd::Outbox {
            action: OutboxCmd::List,
        } => {
            let mut client = connect_client(&args).await?;
            v2_commands::cmd_outbox_list(&client)
                .context("outbox list failed")?;
        }

        Cmd::Outbox {
            action: OutboxCmd::Retry,
        } => {
            let mut client = connect_client(&args).await?;
            v2_commands::cmd_outbox_retry(&mut client)
                .await
                .context("outbox retry failed")?;
        }

        Cmd::Outbox {
            action: OutboxCmd::Clear,
        } => {
            let mut client = connect_client(&args).await?;
            v2_commands::cmd_outbox_clear(&client)
                .context("outbox clear failed")?;
        }
    }

    Ok(())
}
