use std::path::{Path, PathBuf};

use anyhow::Context;
use opaque_ke::{
    ClientLogin, ClientLoginFinishParameters, ClientRegistration,
    ClientRegistrationFinishParameters, CredentialResponse, RegistrationResponse,
};
use quicproquo_core::{
    generate_key_package, hybrid_decrypt, hybrid_encrypt, opaque_auth::OpaqueSuite,
    GroupMember, HybridKeypair, IdentityKeypair, ReceivedMessage,
};

use super::{
    hex,
    rpc::{
        connect_node, current_timestamp_ms, enqueue, fetch_all, fetch_hybrid_key,
        fetch_key_package, fetch_wait, try_hybrid_decrypt, upload_hybrid_key, upload_key_package,
    },
    state::{decode_identity_key, load_existing_state, load_or_init_state, save_state, sha256},
};

/// Print local identity information from the state file (no server connection).
pub fn cmd_whoami(state_path: &Path, password: Option<&str>) -> anyhow::Result<()> {
    let state = load_existing_state(state_path, password)?;
    let identity = IdentityKeypair::from_seed(state.identity_seed);

    let pk_bytes = identity.public_key_bytes();
    let fingerprint = sha256(&pk_bytes);

    println!("identity_key : {}", hex::encode(pk_bytes));
    println!("fingerprint  : {}", hex::encode(fingerprint));
    println!(
        "hybrid_key   : {}",
        if state.hybrid_key.is_some() {
            "present (X25519 + ML-KEM-768)"
        } else {
            "not generated"
        }
    );
    println!(
        "group        : {}",
        if state.group.is_some() {
            "active"
        } else {
            "none"
        }
    );
    println!("state_file   : {}", state_path.display());

    Ok(())
}

/// Check server connectivity via the health RPC.
pub async fn cmd_health(server: &str, ca_cert: &Path, server_name: &str) -> anyhow::Result<()> {
    let sent_at = current_timestamp_ms();
    let client = connect_node(server, ca_cert, server_name).await?;

    let req = client.health_request();
    let resp = req.send().promise.await.context("health RPC failed")?;

    let status = resp
        .get()
        .context("health: bad response")?
        .get_status()
        .context("health: missing status")?
        .to_str()
        .unwrap_or("invalid");

    let rtt_ms = current_timestamp_ms().saturating_sub(sent_at);

    println!("server  : {server}");
    println!("status  : {status}");
    println!("rtt     : {rtt_ms}ms");
    Ok(())
}

/// Check if a peer identity has registered a hybrid public key (non-consuming).
pub async fn cmd_check_key(
    server: &str,
    ca_cert: &Path,
    server_name: &str,
    identity_key_hex: &str,
) -> anyhow::Result<()> {
    let identity_key = decode_identity_key(identity_key_hex)?;
    let node_client = connect_node(server, ca_cert, server_name).await?;

    let hybrid_pk = fetch_hybrid_key(&node_client, &identity_key).await?;

    println!("identity_key : {identity_key_hex}");
    println!(
        "hybrid_key   : {}",
        if hybrid_pk.is_some() {
            "available (X25519 + ML-KEM-768)"
        } else {
            "not found"
        }
    );
    Ok(())
}

/// Connect to `server`, call health, and print RTT over QUIC/TLS.
pub async fn cmd_ping(server: &str, ca_cert: &Path, server_name: &str) -> anyhow::Result<()> {
    let sent_at = current_timestamp_ms();
    let client = connect_node(server, ca_cert, server_name).await?;

    let req = client.health_request();
    let resp = req.send().promise.await.context("health RPC failed")?;

    let status = resp
        .get()
        .context("health: bad response")?
        .get_status()
        .context("health: missing status")?
        .to_str()
        .unwrap_or("invalid");

    let rtt_ms = current_timestamp_ms().saturating_sub(sent_at);
    println!("health={status}  rtt={rtt_ms}ms");
    Ok(())
}

/// Register a new user account via the OPAQUE protocol.
pub async fn cmd_register_user(
    server: &str,
    ca_cert: &Path,
    server_name: &str,
    username: &str,
    password: &str,
    identity_key_hex: Option<&str>,
) -> anyhow::Result<()> {
    let mut rng = rand::rngs::OsRng;

    let node_client = connect_node(server, ca_cert, server_name).await?;

    let identity_key = if let Some(hex_str) = identity_key_hex {
        Some(decode_identity_key(hex_str)?)
    } else {
        None
    };

    let reg_start = ClientRegistration::<OpaqueSuite>::start(&mut rng, password.as_bytes())
        .map_err(|e| anyhow::anyhow!("OPAQUE register start: {e}"))?;

    let mut req = node_client.opaque_register_start_request();
    {
        let mut p = req.get();
        p.set_username(username);
        p.set_request(&reg_start.message.serialize());
    }
    let resp = req
        .send()
        .promise
        .await
        .context("opaque_register_start RPC failed")?;
    let response_bytes = resp
        .get()
        .context("register_start: bad response")?
        .get_response()
        .context("register_start: missing response")?
        .to_vec();

    let reg_response = RegistrationResponse::<OpaqueSuite>::deserialize(&response_bytes)
        .map_err(|e| anyhow::anyhow!("invalid registration response: {e}"))?;

    let reg_finish = reg_start
        .state
        .finish(
            &mut rng,
            password.as_bytes(),
            reg_response,
            ClientRegistrationFinishParameters::<OpaqueSuite>::default(),
        )
        .map_err(|e| anyhow::anyhow!("OPAQUE register finish: {e}"))?;

    let mut req = node_client.opaque_register_finish_request();
    {
        let mut p = req.get();
        p.set_username(username);
        p.set_upload(&reg_finish.message.serialize());
        if let Some(ref ik) = identity_key {
            p.set_identity_key(ik);
        } else {
            p.set_identity_key(&[]);
        }
    }
    let resp = req
        .send()
        .promise
        .await
        .context("opaque_register_finish RPC failed")?;
    let success = resp
        .get()
        .context("register_finish: bad response")?
        .get_success();

    anyhow::ensure!(success, "server rejected registration");

    println!("user '{username}' registered successfully (OPAQUE)");
    if let Some(ik) = identity_key {
        println!("bound identity_key : {}", hex::encode(ik));
    }
    Ok(())
}

/// Log in via the OPAQUE protocol and receive a session token.
#[allow(clippy::too_many_arguments)]
pub async fn cmd_login(
    server: &str,
    ca_cert: &Path,
    server_name: &str,
    username: &str,
    password: &str,
    identity_key_hex: Option<&str>,
    state_path: Option<&Path>,
    state_password: Option<&str>,
) -> anyhow::Result<()> {
    let mut rng = rand::rngs::OsRng;

    let node_client = connect_node(server, ca_cert, server_name).await?;

    let login_start = ClientLogin::<OpaqueSuite>::start(&mut rng, password.as_bytes())
        .map_err(|e| anyhow::anyhow!("OPAQUE login start: {e}"))?;

    let mut req = node_client.opaque_login_start_request();
    {
        let mut p = req.get();
        p.set_username(username);
        p.set_request(&login_start.message.serialize());
    }
    let resp = req
        .send()
        .promise
        .await
        .context("opaque_login_start RPC failed")?;
    let response_bytes = resp
        .get()
        .context("login_start: bad response")?
        .get_response()
        .context("login_start: missing response")?
        .to_vec();

    let credential_response = CredentialResponse::<OpaqueSuite>::deserialize(&response_bytes)
        .map_err(|e| anyhow::anyhow!("invalid credential response: {e}"))?;

    let login_finish = login_start
        .state
        .finish(
            &mut rng,
            password.as_bytes(),
            credential_response,
            ClientLoginFinishParameters::<OpaqueSuite>::default(),
        )
        .map_err(|e| anyhow::anyhow!("OPAQUE login finish (bad password?): {e}"))?;

    let identity_key = derive_identity_for_login(identity_key_hex, state_path, state_password)?;

    let mut req = node_client.opaque_login_finish_request();
    {
        let mut p = req.get();
        p.set_username(username);
        p.set_finalization(&login_finish.message.serialize());
        p.set_identity_key(&identity_key);
    }
    let resp = req
        .send()
        .promise
        .await
        .context("opaque_login_finish RPC failed")?;
    let session_token = resp
        .get()
        .context("login_finish: bad response")?
        .get_session_token()
        .context("login_finish: missing session_token")?
        .to_vec();

    anyhow::ensure!(
        !session_token.is_empty(),
        "server returned empty session token"
    );

    println!("login successful for '{username}'");
    println!("session_token: {}", hex::encode(&session_token));
    println!("(use as --access-token for subsequent commands)");
    Ok(())
}

fn derive_identity_for_login(
    identity_key_hex: Option<&str>,
    state_path: Option<&Path>,
    state_password: Option<&str>,
) -> anyhow::Result<Vec<u8>> {
    if let Some(hex_str) = identity_key_hex {
        let bytes = super::hex::decode(hex_str.trim())
            .map_err(|e| anyhow::anyhow!("identity_key must be 64 hex chars: {e}"))?;
        anyhow::ensure!(
            bytes.len() == 32,
            "identity_key must decode to 32 bytes (got {})",
            bytes.len()
        );
        return Ok(bytes);
    }

    if let Some(path) = state_path {
        let state = load_existing_state(path, state_password)?;
        let identity = IdentityKeypair::from_seed(state.identity_seed);
        return Ok(identity.public_key_bytes().to_vec());
    }

    Err(anyhow::anyhow!(
        "login requires an identity key; pass --identity-key or --state"
    ))
}

// ── OPAQUE helpers (used by both one-shot commands and REPL bootstrap) ───────

/// Perform OPAQUE registration. Returns Ok(()) on success.
/// The error message contains "E018" if the user already exists.
/// Does NOT require init_auth() — OPAQUE RPCs are unauthenticated.
pub(crate) async fn opaque_register(
    client: &quicproquo_proto::node_capnp::node_service::Client,
    username: &str,
    password: &str,
    identity_key: Option<&[u8]>,
) -> anyhow::Result<()> {
    let mut rng = rand::rngs::OsRng;

    let reg_start = ClientRegistration::<OpaqueSuite>::start(&mut rng, password.as_bytes())
        .map_err(|e| anyhow::anyhow!("OPAQUE register start: {e}"))?;

    let mut req = client.opaque_register_start_request();
    {
        let mut p = req.get();
        p.set_username(username);
        p.set_request(&reg_start.message.serialize());
    }
    let resp = req.send().promise.await.context("opaque_register_start RPC failed")?;
    let response_bytes = resp
        .get()
        .context("register_start: bad response")?
        .get_response()
        .context("register_start: missing response")?
        .to_vec();

    let reg_response = RegistrationResponse::<OpaqueSuite>::deserialize(&response_bytes)
        .map_err(|e| anyhow::anyhow!("invalid registration response: {e}"))?;

    let reg_finish = reg_start
        .state
        .finish(
            &mut rng,
            password.as_bytes(),
            reg_response,
            ClientRegistrationFinishParameters::<OpaqueSuite>::default(),
        )
        .map_err(|e| anyhow::anyhow!("OPAQUE register finish: {e}"))?;

    let mut req = client.opaque_register_finish_request();
    {
        let mut p = req.get();
        p.set_username(username);
        p.set_upload(&reg_finish.message.serialize());
        if let Some(ik) = identity_key {
            p.set_identity_key(ik);
        } else {
            p.set_identity_key(&[]);
        }
    }
    let resp = req.send().promise.await.context("opaque_register_finish RPC failed")?;
    let success = resp
        .get()
        .context("register_finish: bad response")?
        .get_success();

    anyhow::ensure!(success, "server rejected registration");
    Ok(())
}

/// Perform OPAQUE login and return the raw session token bytes.
/// Does NOT require init_auth() — OPAQUE RPCs are unauthenticated.
pub async fn opaque_login(
    client: &quicproquo_proto::node_capnp::node_service::Client,
    username: &str,
    password: &str,
    identity_key: &[u8],
) -> anyhow::Result<Vec<u8>> {
    let mut rng = rand::rngs::OsRng;

    let login_start = ClientLogin::<OpaqueSuite>::start(&mut rng, password.as_bytes())
        .map_err(|e| anyhow::anyhow!("OPAQUE login start: {e}"))?;

    let mut req = client.opaque_login_start_request();
    {
        let mut p = req.get();
        p.set_username(username);
        p.set_request(&login_start.message.serialize());
    }
    let resp = req.send().promise.await.context("opaque_login_start RPC failed")?;
    let response_bytes = resp
        .get()
        .context("login_start: bad response")?
        .get_response()
        .context("login_start: missing response")?
        .to_vec();

    let credential_response = CredentialResponse::<OpaqueSuite>::deserialize(&response_bytes)
        .map_err(|e| anyhow::anyhow!("invalid credential response: {e}"))?;

    let login_finish = login_start
        .state
        .finish(
            &mut rng,
            password.as_bytes(),
            credential_response,
            ClientLoginFinishParameters::<OpaqueSuite>::default(),
        )
        .map_err(|e| anyhow::anyhow!("OPAQUE login finish (bad password?): {e}"))?;

    let mut req = client.opaque_login_finish_request();
    {
        let mut p = req.get();
        p.set_username(username);
        p.set_finalization(&login_finish.message.serialize());
        p.set_identity_key(identity_key);
    }
    let resp = req.send().promise.await.context("opaque_login_finish RPC failed")?;
    let session_token = resp
        .get()
        .context("login_finish: bad response")?
        .get_session_token()
        .context("login_finish: missing session_token")?
        .to_vec();

    anyhow::ensure!(!session_token.is_empty(), "server returned empty session token");
    Ok(session_token)
}

/// Generate a KeyPackage for a fresh identity and upload it to the AS.
pub async fn cmd_register(server: &str, ca_cert: &Path, server_name: &str) -> anyhow::Result<()> {
    let identity = IdentityKeypair::generate();

    let (tls_bytes, fingerprint) =
        generate_key_package(&identity).context("KeyPackage generation failed")?;

    let node_client = connect_node(server, ca_cert, server_name).await?;

    let mut req = node_client.upload_key_package_request();
    {
        let mut p = req.get();
        p.set_identity_key(&identity.public_key_bytes());
        p.set_package(&tls_bytes);
        let mut auth = p.reborrow().init_auth();
        super::rpc::set_auth(&mut auth)?;
    }

    let response = req
        .send()
        .promise
        .await
        .context("upload_key_package RPC failed")?;

    let server_fp = response
        .get()
        .context("upload_key_package: bad response")?
        .get_fingerprint()
        .context("upload_key_package: missing fingerprint")?
        .to_vec();

    anyhow::ensure!(
        server_fp == fingerprint,
        "fingerprint mismatch: local={} server={}",
        hex::encode(&fingerprint),
        hex::encode(&server_fp),
    );

    println!(
        "identity_key : {}",
        hex::encode(identity.public_key_bytes())
    );
    println!("fingerprint  : {}", hex::encode(&fingerprint));
    println!("KeyPackage uploaded successfully.");

    Ok(())
}

/// Generate a new KeyPackage from the member, upload it (and optionally hybrid key) to the AS, then save state.
async fn do_upload_keypackage(
    state_path: &Path,
    server: &str,
    ca_cert: &Path,
    server_name: &str,
    password: Option<&str>,
    member: &mut GroupMember,
    hybrid_kp: Option<&HybridKeypair>,
) -> anyhow::Result<()> {
    let tls_bytes = member
        .generate_key_package()
        .context("KeyPackage generation failed")?;
    let fingerprint = sha256(&tls_bytes);

    let node_client = connect_node(server, ca_cert, server_name).await?;

    let mut req = node_client.upload_key_package_request();
    {
        let mut p = req.get();
        p.set_identity_key(&member.identity().public_key_bytes());
        p.set_package(&tls_bytes);
        let mut auth = p.reborrow().init_auth();
        super::rpc::set_auth(&mut auth)?;
    }

    let response = req
        .send()
        .promise
        .await
        .context("upload_key_package RPC failed")?;

    let server_fp = response
        .get()
        .context("upload_key_package: bad response")?
        .get_fingerprint()
        .context("upload_key_package: missing fingerprint")?
        .to_vec();

    anyhow::ensure!(server_fp == fingerprint, "fingerprint mismatch");

    if let Some(hkp) = &hybrid_kp {
        upload_hybrid_key(
            &node_client,
            &member.identity().public_key_bytes(),
            &hkp.public_key(),
        )
        .await?;
        println!("hybrid_key   : uploaded (X25519 + ML-KEM-768)");
    }

    println!(
        "identity_key : {}",
        hex::encode(member.identity().public_key_bytes())
    );
    println!("fingerprint  : {}", hex::encode(&fingerprint));
    println!("KeyPackage uploaded successfully.");

    save_state(state_path, member, hybrid_kp, password)?;
    Ok(())
}

/// Upload the stored identity's KeyPackage to the AS.
/// Creates state (and identity) if the state file does not exist; otherwise uses existing state.
pub async fn cmd_register_state(
    state_path: &Path,
    server: &str,
    ca_cert: &Path,
    server_name: &str,
    password: Option<&str>,
) -> anyhow::Result<()> {
    let state = load_or_init_state(state_path, password)?;
    let (mut member, hybrid_kp) = state.into_parts(state_path)?;
    do_upload_keypackage(
        state_path,
        server,
        ca_cert,
        server_name,
        password,
        &mut member,
        hybrid_kp.as_ref(),
    )
    .await
}

/// Refresh the KeyPackage on the server (load existing state, generate new KeyPackage, upload).
/// Use this when your KeyPackage has expired (e.g. server TTL ~24h) or was consumed by an invite.
/// Requires an existing state file; does not create a new identity.
pub async fn cmd_refresh_keypackage(
    state_path: &Path,
    server: &str,
    ca_cert: &Path,
    server_name: &str,
    password: Option<&str>,
) -> anyhow::Result<()> {
    let state = load_existing_state(state_path, password)?;
    let (mut member, hybrid_kp) = state.into_parts(state_path)?;
    do_upload_keypackage(
        state_path,
        server,
        ca_cert,
        server_name,
        password,
        &mut member,
        hybrid_kp.as_ref(),
    )
    .await
}

/// Fetch a peer's KeyPackage from the AS.
pub async fn cmd_fetch_key(
    server: &str,
    ca_cert: &Path,
    server_name: &str,
    identity_key_hex: &str,
) -> anyhow::Result<()> {
    let identity_key = super::hex::decode(identity_key_hex)
        .map_err(|e| anyhow::anyhow!(e))
        .context("identity_key must be 64 hex characters (32 bytes)")?;
    anyhow::ensure!(
        identity_key.len() == 32,
        "identity_key must be exactly 32 bytes, got {}",
        identity_key.len()
    );

    let node_client = connect_node(server, ca_cert, server_name).await?;

    let mut req = node_client.fetch_key_package_request();
    {
        let mut p = req.get();
        p.set_identity_key(&identity_key);
        let mut auth = p.reborrow().init_auth();
        super::rpc::set_auth(&mut auth)?;
    }

    let response = req
        .send()
        .promise
        .await
        .context("fetch_key_package RPC failed")?;

    let package = response
        .get()
        .context("fetch_key_package: bad response")?
        .get_package()
        .context("fetch_key_package: missing package field")?
        .to_vec();

    if package.is_empty() {
        println!("No KeyPackage available for this identity.");
        return Ok(());
    }

    use sha2::{Digest, Sha256};
    let fingerprint = Sha256::digest(&package);

    println!("fingerprint  : {}", hex::encode(fingerprint));
    println!("package_len  : {} bytes", package.len());
    println!("KeyPackage fetched successfully.");

    Ok(())
}

/// Run a two-party MLS demo against the unified server.
pub async fn cmd_demo_group(server: &str, ca_cert: &Path, server_name: &str) -> anyhow::Result<()> {
    let creator_state_path = PathBuf::from("qpq-demo-creator.bin");
    let joiner_state_path = PathBuf::from("qpq-demo-joiner.bin");

    let (mut creator, creator_hybrid_opt) =
        load_or_init_state(&creator_state_path, None)?.into_parts(&creator_state_path)?;
    let (mut joiner, joiner_hybrid_opt) =
        load_or_init_state(&joiner_state_path, None)?.into_parts(&joiner_state_path)?;

    let creator_hybrid = creator_hybrid_opt.unwrap_or_else(HybridKeypair::generate);
    let joiner_hybrid = joiner_hybrid_opt.unwrap_or_else(HybridKeypair::generate);

    let creator_kp = creator
        .generate_key_package()
        .context("creator KeyPackage generation failed")?;
    let joiner_kp = joiner
        .generate_key_package()
        .context("joiner KeyPackage generation failed")?;

    let creator_node = connect_node(server, ca_cert, server_name).await?;
    let joiner_node = connect_node(server, ca_cert, server_name).await?;

    let creator_identity = creator.identity().public_key_bytes();
    let joiner_identity = joiner.identity().public_key_bytes();

    upload_key_package(&creator_node, &creator_identity, &creator_kp).await?;
    upload_key_package(&joiner_node, &joiner_identity, &joiner_kp).await?;
    upload_hybrid_key(&creator_node, &creator_identity, &creator_hybrid.public_key()).await?;
    upload_hybrid_key(&joiner_node, &joiner_identity, &joiner_hybrid.public_key()).await?;

    println!("hybrid public keys uploaded for creator and joiner");

    let fetched_joiner_kp = fetch_key_package(&creator_node, &joiner_identity).await?;
    anyhow::ensure!(
        !fetched_joiner_kp.is_empty(),
        "AS returned an empty KeyPackage for joiner",
    );

    creator
        .create_group(b"demo-group")
        .context("create_group failed")?;
    let (_commit, welcome) = creator
        .add_member(&fetched_joiner_kp)
        .context("add_member failed")?;

    let creator_ds = creator_node.clone();
    let joiner_ds = joiner_node.clone();

    let joiner_hybrid_pk = fetch_hybrid_key(&creator_node, &joiner_identity)
        .await?
        .context("joiner hybrid key not found")?;
    let wrapped_welcome =
        hybrid_encrypt(&joiner_hybrid_pk, &welcome, b"", b"").context("hybrid encrypt welcome")?;
    enqueue(&creator_ds, &joiner_identity, &wrapped_welcome).await?;

    let welcome_payloads = fetch_all(&joiner_ds, &joiner_identity).await?;
    let raw_welcome = welcome_payloads
        .first()
        .map(|(_, d)| d.clone())
        .context("Welcome was not delivered to joiner via DS")?;

    let welcome_bytes =
        hybrid_decrypt(&joiner_hybrid, &raw_welcome, b"", b"").context("hybrid decrypt welcome failed")?;
    joiner
        .join_group(&welcome_bytes)
        .context("join_group failed")?;

    let ct_creator_to_joiner = creator
        .send_message(b"hello")
        .context("send_message failed")?;
    let wrapped_creator_joiner =
        hybrid_encrypt(&joiner_hybrid_pk, &ct_creator_to_joiner, b"", b"").context("hybrid encrypt failed")?;
    enqueue(&creator_ds, &joiner_identity, &wrapped_creator_joiner).await?;

    let joiner_msgs = fetch_all(&joiner_ds, &joiner_identity).await?;
    let (_, raw_creator_joiner) = joiner_msgs
        .first()
        .context("joiner: missing ciphertext from DS")?;
    let inner_creator_joiner =
        hybrid_decrypt(&joiner_hybrid, raw_creator_joiner, b"", b"").context("hybrid decrypt failed")?;
    let plaintext_creator_joiner = match joiner.receive_message(&inner_creator_joiner)? {
        ReceivedMessage::Application(pt) => pt,
        other => anyhow::bail!("expected application message, got {other:?}"),
    };
    println!(
        "creator -> joiner plaintext: {}",
        String::from_utf8_lossy(&plaintext_creator_joiner)
    );

    let creator_hybrid_pk = fetch_hybrid_key(&joiner_node, &creator_identity)
        .await?
        .context("creator hybrid key not found")?;
    let ct_joiner_to_creator = joiner
        .send_message(b"hello back")
        .context("send_message failed")?;
    let wrapped_joiner_creator =
        hybrid_encrypt(&creator_hybrid_pk, &ct_joiner_to_creator, b"", b"").context("hybrid encrypt failed")?;
    enqueue(&joiner_ds, &creator_identity, &wrapped_joiner_creator).await?;

    let creator_msgs = fetch_all(&creator_ds, &creator_identity).await?;
    let (_, raw_joiner_creator) = creator_msgs
        .first()
        .context("creator: missing ciphertext from DS")?;
    let inner_joiner_creator =
        hybrid_decrypt(&creator_hybrid, raw_joiner_creator, b"", b"").context("hybrid decrypt failed")?;
    let plaintext_joiner_creator = match creator.receive_message(&inner_joiner_creator)? {
        ReceivedMessage::Application(pt) => pt,
        other => anyhow::bail!("expected application message, got {other:?}"),
    };
    println!(
        "joiner -> creator plaintext: {}",
        String::from_utf8_lossy(&plaintext_joiner_creator)
    );

    println!("demo-group complete (hybrid PQ envelope active)");

    Ok(())
}

/// Create a new group and persist state.
pub async fn cmd_create_group(
    state_path: &Path,
    _server: &str,
    group_id: &str,
    password: Option<&str>,
) -> anyhow::Result<()> {
    let state = load_or_init_state(state_path, password)?;
    let (mut member, hybrid_kp) = state.into_parts(state_path)?;

    anyhow::ensure!(
        member.group_ref().is_none(),
        "group already exists in state"
    );

    member
        .create_group(group_id.as_bytes())
        .context("create_group failed")?;

    save_state(state_path, &member, hybrid_kp.as_ref(), password)?;
    println!("group created: {group_id}");
    Ok(())
}

/// Invite a peer: fetch their KeyPackage, add to group, enqueue Welcome.
pub async fn cmd_invite(
    state_path: &Path,
    server: &str,
    ca_cert: &Path,
    server_name: &str,
    peer_key_hex: &str,
    password: Option<&str>,
) -> anyhow::Result<()> {
    let state = load_existing_state(state_path, password)?;
    let (mut member, hybrid_kp) = state.into_parts(state_path)?;

    let peer_key = decode_identity_key(peer_key_hex)?;
    let node_client = connect_node(server, ca_cert, server_name).await?;
    let peer_kp = fetch_key_package(&node_client, &peer_key).await?;
    anyhow::ensure!(
        !peer_kp.is_empty(),
        "server returned empty KeyPackage for peer"
    );

    let _ = member
        .group_ref()
        .context("no active group; run create-group first")?;

    let existing_members: Vec<Vec<u8>> = member
        .member_identities()
        .into_iter()
        .filter(|k| k.as_slice() != member.identity().public_key_bytes())
        .collect();

    let (commit, welcome) = member.add_member(&peer_kp).context("add_member failed")?;

    for mk in &existing_members {
        if mk.as_slice() == peer_key.as_slice() {
            continue;
        }
        let peer_hpk = fetch_hybrid_key(&node_client, mk).await?;
        let commit_payload = if let Some(ref pk) = peer_hpk {
            hybrid_encrypt(pk, &commit, b"", b"").context("hybrid encrypt commit")?
        } else {
            commit.clone()
        };
        enqueue(&node_client, mk, &commit_payload).await?;
    }

    let peer_hybrid_pk = fetch_hybrid_key(&node_client, &peer_key).await?;
    let payload = if let Some(ref pk) = peer_hybrid_pk {
        hybrid_encrypt(pk, &welcome, b"", b"").context("hybrid encrypt welcome failed")?
    } else {
        welcome
    };

    enqueue(&node_client, &peer_key, &payload).await?;

    save_state(state_path, &member, hybrid_kp.as_ref(), password)?;
    println!(
        "invited peer (welcome queued{}, commit sent to {} existing member(s))",
        if peer_hybrid_pk.is_some() {
            ", hybrid-encrypted"
        } else {
            ""
        },
        existing_members.len(),
    );
    Ok(())
}

/// Join a group by consuming a Welcome from the server queue.
/// If the queue contained [Welcome, Commit, ...] (e.g. creator invited someone else before this
/// joiner ran), we use the first payload as Welcome and process the rest in order (merge Commits)
/// so the joiner's epoch matches the creator before any later app messages are received.
pub async fn cmd_join(
    state_path: &Path,
    server: &str,
    ca_cert: &Path,
    server_name: &str,
    password: Option<&str>,
) -> anyhow::Result<()> {
    let state = load_existing_state(state_path, password)?;
    let (mut member, hybrid_kp) = state.into_parts(state_path)?;

    anyhow::ensure!(
        member.group_ref().is_none(),
        "group already active in state"
    );

    let node_client = connect_node(server, ca_cert, server_name).await?;
    let mut payloads = fetch_all(&node_client, &member.identity().public_key_bytes()).await?;
    payloads.sort_by_key(|(seq, _)| *seq);
    let (_, raw_welcome) = payloads
        .first()
        .cloned()
        .context("no Welcome found in DS for this identity")?;

    let welcome_bytes = try_hybrid_decrypt(hybrid_kp.as_ref(), &raw_welcome)
        .context("decrypt Welcome (hybrid required)")?;

    member
        .join_group(&welcome_bytes)
        .context("join_group failed")?;

    // Process any remaining payloads (e.g. Commit from creator adding another member) in order
    // so our epoch matches before we later receive application messages.
    for (_, raw) in payloads.iter().skip(1) {
        let mls_payload = match try_hybrid_decrypt(hybrid_kp.as_ref(), raw) {
            Ok(b) => b,
            Err(_) => continue,
        };
        let _ = member.receive_message(&mls_payload);
    }

    // Auto-replenish KeyPackage after join consumed the original one.
    let tls_bytes = member
        .generate_key_package()
        .context("KeyPackage replenishment failed")?;
    upload_key_package(&node_client, &member.identity().public_key_bytes(), &tls_bytes)
        .await
        .context("KeyPackage replenishment upload failed")?;
    println!("KeyPackage auto-replenished after join");

    save_state(state_path, &member, hybrid_kp.as_ref(), password)?;
    println!("joined group successfully");
    Ok(())
}

/// Send an application message via DS (single recipient or broadcast to all other members).
#[allow(clippy::too_many_arguments)]
pub async fn cmd_send(
    state_path: &Path,
    server: &str,
    ca_cert: &Path,
    server_name: &str,
    peer_key_hex: Option<&str>,
    send_to_all: bool,
    msg: &str,
    password: Option<&str>,
) -> anyhow::Result<()> {
    let state = load_existing_state(state_path, password)?;
    let (mut member, hybrid_kp) = state.into_parts(state_path)?;

    let _ = member
        .group_ref()
        .context("no active group; create one and invite members first")?;

    let node_client = connect_node(server, ca_cert, server_name).await?;

    let ct = member
        .send_message(msg.as_bytes())
        .context("send_message failed")?;

    let my_identity = member.identity().public_key_bytes();
    let recipients: Vec<Vec<u8>> = if send_to_all {
        member
            .member_identities()
            .into_iter()
            .filter(|k| k.as_slice() != my_identity)
            .collect()
    } else {
        let peer_key = decode_identity_key(
            peer_key_hex.context("peer_key required when not using --all")?,
        )?;
        vec![peer_key]
    };

    for recipient in &recipients {
        let peer_hybrid_pk = fetch_hybrid_key(&node_client, recipient).await?;
        let payload = if let Some(ref pk) = peer_hybrid_pk {
            hybrid_encrypt(pk, &ct, b"", b"").context("hybrid encrypt failed")?
        } else {
            ct.clone()
        };
        enqueue(&node_client, recipient, &payload).await?;
    }

    save_state(state_path, &member, hybrid_kp.as_ref(), password)?;
    println!(
        "message sent to {} recipient(s){}",
        recipients.len(),
        if recipients.len() == 1 {
            " (hybrid-encrypted)"
        } else {
            ""
        }
    );
    Ok(())
}

/// Receive and decrypt all pending messages from the server.
pub async fn cmd_recv(
    state_path: &Path,
    server: &str,
    ca_cert: &Path,
    server_name: &str,
    wait_ms: u64,
    stream: bool,
    password: Option<&str>,
) -> anyhow::Result<()> {
    let state = load_existing_state(state_path, password)?;
    let (mut member, hybrid_kp) = state.into_parts(state_path)?;

    let client = connect_node(server, ca_cert, server_name).await?;

    loop {
        let mut payloads =
            fetch_wait(&client, &member.identity().public_key_bytes(), wait_ms).await?;

        if payloads.is_empty() {
            if !stream {
                println!("no messages");
                return Ok(());
            }
            continue;
        }

        // Sort by server-assigned sequence number so MLS commits arrive before
        // application messages that depend on the resulting epoch.
        payloads.sort_by_key(|(seq, _)| *seq);

        let mut pending: Vec<(usize, Vec<u8>)> = Vec::new();
        for (idx, (_, payload)) in payloads.iter().enumerate() {
            let mls_payload = match try_hybrid_decrypt(hybrid_kp.as_ref(), payload) {
                Ok(b) => b,
                Err(e) => {
                    println!("[{idx}] decrypt error: {e}");
                    continue;
                }
            };
            match member.receive_message(&mls_payload) {
                Ok(ReceivedMessage::Application(pt)) => println!("[{idx}] plaintext: {}", String::from_utf8_lossy(&pt)),
                Ok(ReceivedMessage::StateChanged) | Ok(ReceivedMessage::SelfRemoved) => println!("[{idx}] commit applied"),
                Err(_) => pending.push((idx, mls_payload)),
            }
        }
        // Retry until no more progress (handles multi-epoch batches).
        loop {
            let before = pending.len();
            pending.retain(|(idx, mls_payload)| {
                match member.receive_message(mls_payload) {
                    Ok(ReceivedMessage::Application(pt)) => {
                        println!("[{idx}/retry] plaintext: {}", String::from_utf8_lossy(&pt));
                        false
                    }
                    Ok(ReceivedMessage::StateChanged) | Ok(ReceivedMessage::SelfRemoved) => {
                        println!("[{idx}/retry] commit applied");
                        false
                    }
                    Err(_) => true,
                }
            });
            if pending.len() == before {
                break; // No progress — remaining messages are unprocessable
            }
        }
        for (idx, _) in &pending {
            println!("[{idx}] error: unprocessable after all retries");
        }

        save_state(state_path, &member, hybrid_kp.as_ref(), password)?;

        if !stream {
            return Ok(());
        }
    }
}

/// Fetch pending payloads, process in order (merge commits, collect plaintexts), save state.
/// Returns only application-message plaintexts. Used by E2E tests and callers that need returned messages.
/// Retries in a loop until no more progress, handling multi-epoch batches where commits must be
/// applied before later application messages can be decrypted.
pub async fn receive_pending_plaintexts(
    state_path: &Path,
    server: &str,
    ca_cert: &Path,
    server_name: &str,
    wait_ms: u64,
    password: Option<&str>,
) -> anyhow::Result<Vec<Vec<u8>>> {
    let state = load_existing_state(state_path, password)?;
    let (mut member, hybrid_kp) = state.into_parts(state_path)?;

    let client = connect_node(server, ca_cert, server_name).await?;
    let mut payloads =
        fetch_wait(&client, &member.identity().public_key_bytes(), wait_ms).await?;
    payloads.sort_by_key(|(seq, _)| *seq);

    let mut plaintexts = Vec::new();
    let mut pending: Vec<Vec<u8>> = Vec::new();
    for (_, payload) in &payloads {
        let mls_payload = match try_hybrid_decrypt(hybrid_kp.as_ref(), payload) {
            Ok(b) => b,
            Err(_) => continue,
        };
        match member.receive_message(&mls_payload) {
            Ok(ReceivedMessage::Application(pt)) => plaintexts.push(pt),
            Ok(ReceivedMessage::StateChanged) | Ok(ReceivedMessage::SelfRemoved) => {}
            Err(_) => pending.push(mls_payload),
        }
    }
    // Retry until no more progress (handles multi-epoch batches).
    loop {
        let before = pending.len();
        pending.retain(|mls_payload| {
            match member.receive_message(mls_payload) {
                Ok(ReceivedMessage::Application(pt)) => {
                    plaintexts.push(pt);
                    false
                }
                Ok(ReceivedMessage::StateChanged) | Ok(ReceivedMessage::SelfRemoved) => false,
                Err(_) => true,
            }
        });
        if pending.len() == before {
            break;
        }
    }

    save_state(state_path, &member, hybrid_kp.as_ref(), password)?;
    Ok(plaintexts)
}

/// JSON-returning whoami for GUI backends.
pub fn whoami_json(state_path: &Path, password: Option<&str>) -> anyhow::Result<String> {
    let state = load_existing_state(state_path, password)?;
    let identity = IdentityKeypair::from_seed(state.identity_seed);
    let pk_bytes = identity.public_key_bytes();
    let fingerprint = sha256(&pk_bytes);
    Ok(format!(
        r#"{{"identity_key":"{}", "fingerprint":"{}", "hybrid_key":{}, "group":{}}}"#,
        hex::encode(pk_bytes),
        hex::encode(fingerprint),
        state.hybrid_key.is_some(),
        state.group.is_some(),
    ))
}

/// JSON-returning health check for GUI backends.
pub async fn cmd_health_json(server: &str, ca_cert: &Path, server_name: &str) -> anyhow::Result<String> {
    let sent_at = current_timestamp_ms();
    let client = connect_node(server, ca_cert, server_name).await?;
    let req = client.health_request();
    let resp = req.send().promise.await.context("health RPC failed")?;
    let status = resp
        .get()
        .context("health: bad response")?
        .get_status()
        .context("health: missing status")?
        .to_str()
        .unwrap_or("invalid");
    let rtt_ms = current_timestamp_ms().saturating_sub(sent_at);
    Ok(format!(r#"{{"status":"{status}","rtt_ms":{rtt_ms}}}"#))
}

/// Run an interactive 1:1 chat session.
pub async fn cmd_chat(
    state_path: &Path,
    server: &str,
    ca_cert: &Path,
    server_name: &str,
    peer_key_hex: Option<&str>,
    password: Option<&str>,
    poll_interval_ms: u64,
) -> anyhow::Result<()> {
    use std::io::Write;
    use tokio::io::AsyncBufReadExt;
    use tokio::sync::mpsc;
    use tokio::time::interval;

    let state = load_existing_state(state_path, password)?;
    let (mut member, hybrid_kp) = state.into_parts(state_path)?;

    let _group = member
        .group_ref()
        .context("no active group; create one and invite the peer first")?;

    let my_identity = member.identity().public_key_bytes();
    let peer_key: Vec<u8> = match peer_key_hex {
        Some(h) => decode_identity_key(h)?,
        None => {
            let others: Vec<Vec<u8>> = member
                .member_identities()
                .into_iter()
                .filter(|id| id.as_slice() != my_identity)
                .collect();
            match others.as_slice() {
                [single] => single.clone(),
                [] => anyhow::bail!(
                    "group has no other member; invite someone first, or pass --peer-key"
                ),
                _ => anyhow::bail!(
                    "group has {} other members; pass --peer-key <identity_hex> to pick one",
                    others.len()
                ),
            }
        }
    };
    let identity_bytes = member.identity().public_key_bytes().to_vec();

    let client = connect_node(server, ca_cert, server_name).await?;

    let (tx, mut rx) = mpsc::unbounded_channel::<Option<String>>();

    tokio::task::spawn_local({
        let tx = tx.clone();
        async move {
            let mut stdin = tokio::io::BufReader::new(tokio::io::stdin());
            let mut line = String::new();
            loop {
                line.clear();
                match stdin.read_line(&mut line).await {
                    Ok(0) => {
                        let _ = tx.send(None);
                        break;
                    }
                    Ok(_) => {
                        let trimmed = line.trim().to_string();
                        let _ = tx.send(Some(trimmed));
                    }
                    Err(_) => break,
                }
            }
        }
    });

    let mut poll = interval(std::time::Duration::from_millis(poll_interval_ms));
    poll.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);

    println!("Chat with peer {} (Ctrl+D to exit)", hex::encode(&peer_key[..8]));
    print!("> ");
    std::io::stdout().flush().context("flush stdout")?;

    loop {
        tokio::select! {
            msg = rx.recv() => {
                match msg {
                    Some(None) => break,
                    Some(Some(line)) => {
                        if !line.is_empty() {
                            let ct = member
                                .send_message(line.as_bytes())
                                .context("send_message failed")?;
                            let peer_hybrid_pk = fetch_hybrid_key(&client, &peer_key).await?;
                            let payload = if let Some(ref pk) = peer_hybrid_pk {
                                hybrid_encrypt(pk, &ct, b"", b"").context("hybrid encrypt failed")?
                            } else {
                                ct
                            };
                            enqueue(&client, &peer_key, &payload).await?;
                            save_state(state_path, &member, hybrid_kp.as_ref(), password)?;
                        }
                        print!("> ");
                        std::io::stdout().flush().context("flush stdout")?;
                    }
                    None => break,
                }
            }
            _ = poll.tick() => {
                let mut payloads = fetch_wait(&client, &identity_bytes, 0).await?;
                payloads.sort_by_key(|(seq, _)| *seq);
                let mut retry_payloads: Vec<Vec<u8>> = Vec::new();
                for (_, payload) in &payloads {
                    let mls_payload = match try_hybrid_decrypt(hybrid_kp.as_ref(), payload) {
                        Ok(b) => b,
                        Err(_) => continue,
                    };
                    match member.receive_message(&mls_payload) {
                        Ok(ReceivedMessage::Application(pt)) => {
                            let s = String::from_utf8_lossy(&pt);
                            println!("\r\n[peer] {s}\n> ");
                            std::io::stdout().flush().context("flush stdout")?;
                        }
                        Ok(ReceivedMessage::StateChanged) | Ok(ReceivedMessage::SelfRemoved) => {}
                        Err(_) => retry_payloads.push(mls_payload),
                    }
                }
                // Retry failed messages (epoch may have advanced from commits in this batch)
                loop {
                    let before = retry_payloads.len();
                    retry_payloads.retain(|mls_payload| {
                        match member.receive_message(mls_payload) {
                            Ok(ReceivedMessage::Application(pt)) => {
                                let s = String::from_utf8_lossy(&pt);
                                println!("\r\n[peer] {s}\n> ");
                                let _ = std::io::stdout().flush();
                                false
                            }
                            Ok(ReceivedMessage::StateChanged) | Ok(ReceivedMessage::SelfRemoved) => false,
                            Err(_) => true,
                        }
                    });
                    if retry_payloads.len() == before { break; }
                }
                if !payloads.is_empty() {
                    save_state(state_path, &member, hybrid_kp.as_ref(), password)?;
                }
            }
        }
    }

    println!();
    Ok(())
}

// ── Transcript export ─────────────────────────────────────────────────────────

/// Export the message history for a conversation to an encrypted, tamper-evident
/// transcript file.
///
/// `conv_db` is the path to the conversation SQLite database (`.convdb` file).
/// `conv_id_hex` is the 32-hex-character conversation ID to export.
/// `output` is the path for the `.qpqt` transcript file to write.
/// `transcript_password` is used to derive the encryption key (Argon2id).
/// `db_password` is the optional SQLCipher password for the conversation database.
pub fn cmd_export(
    conv_db: &Path,
    conv_id_hex: &str,
    output: &Path,
    transcript_password: &str,
    db_password: Option<&str>,
) -> anyhow::Result<()> {
    use quicproquo_core::{TranscriptRecord, TranscriptWriter};
    use super::conversation::{ConversationId, ConversationStore};

    // Decode conversation ID from hex.
    let id_bytes = hex::decode(conv_id_hex)
        .map_err(|e| anyhow::anyhow!("conv-id must be 32 hex characters (16 bytes): {e}"))?;
    let conv_id = ConversationId::from_slice(&id_bytes)
        .ok_or_else(|| anyhow::anyhow!("conv-id must be exactly 16 bytes (32 hex chars), got {} bytes", id_bytes.len()))?;

    // Open conversation database.
    let store = ConversationStore::open(conv_db, db_password)
        .context("open conversation database")?;

    // Load conversation metadata (to display name in output).
    let conv = store
        .load_conversation(&conv_id)?
        .with_context(|| format!("conversation '{conv_id_hex}' not found in database"))?;

    // Load all messages (oldest first).
    let messages = store.load_all_messages(&conv_id)?;

    if messages.is_empty() {
        println!("No messages in conversation '{}'.", conv.display_name);
        return Ok(());
    }

    // Create output file.
    if let Some(parent) = output.parent() {
        std::fs::create_dir_all(parent).ok();
    }
    let mut file = std::fs::File::create(output)
        .with_context(|| format!("create transcript file '{}'", output.display()))?;

    // Write transcript header + records.
    let mut writer = TranscriptWriter::new(transcript_password, &mut file)
        .context("initialise transcript writer")?;

    let mut written = 0u64;
    for (seq, msg) in messages.iter().enumerate() {
        writer
            .write_record(
                &TranscriptRecord {
                    seq: seq as u64,
                    sender_identity: &msg.sender_key,
                    timestamp_ms: msg.timestamp_ms,
                    plaintext: &msg.body,
                },
                &mut file,
            )
            .context("write transcript record")?;
        written += 1;
    }

    println!(
        "Exported {} message(s) from '{}' to '{}'.",
        written,
        conv.display_name,
        output.display()
    );
    println!("Decrypt with: qpq export verify --input <file> --password <password>");

    Ok(())
}

/// Verify the hash-chain integrity of a transcript file without decrypting content.
///
/// Prints a summary. Does not require the encryption password (structural check only).
pub fn cmd_export_verify(input: &Path) -> anyhow::Result<()> {
    use quicproquo_core::{validate_transcript_structure, ChainVerdict};

    let data = std::fs::read(input)
        .with_context(|| format!("read transcript file '{}'", input.display()))?;

    match validate_transcript_structure(&data)? {
        ChainVerdict::Ok { records } => {
            println!(
                "OK: transcript '{}' is structurally valid. {} record(s) found, hash chain intact.",
                input.display(),
                records
            );
        }
        ChainVerdict::Broken => {
            anyhow::bail!(
                "FAIL: hash chain is broken in '{}' — file may have been tampered with.",
                input.display()
            );
        }
    }
    Ok(())
}
