//! Account recovery — setup, upload, and restore via recovery codes.
//!
//! Wraps `quicproquo_core::recovery` and the v2 RPC recovery service.

use bytes::Bytes;
use prost::Message;
use quicproquo_core::recovery::{
    generate_recovery_codes, recover_from_bundle, recovery_token_hash, RecoveryBundle,
};
use quicproquo_proto::{method_ids, qpq::v1};
use quicproquo_rpc::client::RpcClient;

use crate::error::SdkError;

/// Set up account recovery: generate codes, encrypt bundles, upload to server.
///
/// Returns the recovery codes (display to user once, never store).
pub async fn setup_recovery(
    rpc: &RpcClient,
    identity_seed: &[u8; 32],
    conversation_ids: &[Vec<u8>],
) -> Result<Vec<String>, SdkError> {
    let setup = generate_recovery_codes(identity_seed, conversation_ids)
        .map_err(|e| SdkError::Crypto(format!("recovery code generation: {e}")))?;

    // Upload each encrypted bundle to the server.
    for bundle in &setup.bundles {
        let bundle_bytes = bincode::serialize(bundle)
            .map_err(|e| SdkError::Crypto(format!("serialize recovery bundle: {e}")))?;

        let req = v1::StoreRecoveryBundleRequest {
            token_hash: bundle.token_hash.clone(),
            bundle: bundle_bytes,
            ttl_secs: 0, // Use server default (90 days).
        };

        let resp_bytes = rpc
            .call(
                method_ids::STORE_RECOVERY_BUNDLE,
                Bytes::from(req.encode_to_vec()),
            )
            .await?;

        let resp = v1::StoreRecoveryBundleResponse::decode(resp_bytes)
            .map_err(|e| SdkError::Crypto(format!("decode store_recovery response: {e}")))?;

        if !resp.success {
            return Err(SdkError::Crypto(
                "server rejected recovery bundle upload".into(),
            ));
        }
    }

    Ok(setup.codes)
}

/// Recover an account from a recovery code.
///
/// Fetches the encrypted bundle from the server, decrypts it with the code,
/// and returns the identity seed and conversation IDs.
pub async fn recover_account(
    rpc: &RpcClient,
    code: &str,
) -> Result<(/* identity_seed */ [u8; 32], /* conversation_ids */ Vec<Vec<u8>>), SdkError> {
    // Compute the token hash for server-side lookup.
    let token_hash = recovery_token_hash(code);

    let req = v1::FetchRecoveryBundleRequest {
        token_hash: token_hash.clone(),
    };

    let resp_bytes = rpc
        .call(
            method_ids::FETCH_RECOVERY_BUNDLE,
            Bytes::from(req.encode_to_vec()),
        )
        .await?;

    let resp = v1::FetchRecoveryBundleResponse::decode(resp_bytes)
        .map_err(|e| SdkError::Crypto(format!("decode fetch_recovery response: {e}")))?;

    if resp.bundle.is_empty() {
        return Err(SdkError::Crypto(
            "no recovery bundle found for this code".into(),
        ));
    }

    // Deserialize the bundle.
    let bundle: RecoveryBundle = bincode::deserialize(&resp.bundle)
        .map_err(|e| SdkError::Crypto(format!("deserialize recovery bundle: {e}")))?;

    // Decrypt with the code.
    let payload = recover_from_bundle(code, &bundle)
        .map_err(|e| SdkError::Crypto(format!("recovery decryption failed: {e}")))?;

    Ok((payload.identity_seed, payload.conversation_ids))
}

/// Delete all recovery bundles for the given codes (e.g. after refresh).
pub async fn delete_recovery_bundles(
    rpc: &RpcClient,
    codes: &[String],
) -> Result<(), SdkError> {
    for code in codes {
        let token_hash = recovery_token_hash(code);
        let req = v1::DeleteRecoveryBundleRequest { token_hash };

        let resp_bytes = rpc
            .call(
                method_ids::DELETE_RECOVERY_BUNDLE,
                Bytes::from(req.encode_to_vec()),
            )
            .await?;

        let _resp = v1::DeleteRecoveryBundleResponse::decode(resp_bytes)
            .map_err(|e| SdkError::Crypto(format!("decode delete_recovery response: {e}")))?;
    }
    Ok(())
}
