//! Async RPC-based account discovery.
//!
//! Takes candidate accounts generated by the KMS discovery module and checks
//! which ones are actually deployed on-chain via the Starknet JSON-RPC API.

use std::collections::HashSet;
use std::sync::Arc;

use krusty_kms::discovery::{CandidateAccount, WalletType};
use krusty_kms_common::{KmsError, Result};
use serde::{Deserialize, Serialize};
use starknet_rust::core::types::{BlockId, BlockTag, StarknetError};
use starknet_rust::providers::{Provider, ProviderError};

use crate::wallet::utils::{core_felt_to_rs, rs_felt_to_core, CoreFelt};

/// An account that was found deployed on-chain.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiscoveredAccount {
    /// The wallet type that generated this account.
    pub wallet_type: WalletType,
    /// The class hash used for address derivation.
    pub class_hash: String,
    /// The on-chain account address.
    pub address: String,
    /// The Stark public key.
    pub public_key: String,
    /// The Stark private key. Handle with care.
    pub private_key: String,
    /// The HD derivation index.
    pub derivation_index: u32,
    /// Human-readable derivation path.
    pub derivation_path: String,
    /// Class hash version label.
    pub class_version: String,
    /// The actual class hash deployed on-chain (may differ from derivation class hash
    /// if the account was upgraded after deployment).
    pub deployed_class_hash: String,
}

impl DiscoveredAccount {
    /// Construct a [`DiscoveredAccount`] from a [`CandidateAccount`] and the
    /// on-chain class hash returned by the RPC.
    fn from_candidate(candidate: &CandidateAccount, deployed_class_hash: String) -> Self {
        Self {
            wallet_type: candidate.wallet_type,
            class_hash: candidate.class_hash.clone(),
            address: candidate.address.clone(),
            public_key: candidate.public_key.clone(),
            private_key: candidate.private_key.clone(),
            derivation_index: candidate.derivation_index,
            derivation_path: candidate.derivation_path.clone(),
            class_version: candidate.class_version.clone(),
            deployed_class_hash,
        }
    }
}

/// Discover all Starknet accounts for a mnemonic by checking on-chain deployment.
///
/// This is the main entry point for account recovery. It:
/// 1. Generates all candidate addresses using `krusty_kms::discovery::generate_candidates`
/// 2. Checks each candidate against the Starknet RPC to see if it's deployed
/// 3. Returns only the accounts that exist on-chain, with their actual class hash
///
/// Uses sequential RPC calls to avoid rate-limiting issues common with RPC providers.
/// Results are deduplicated by address so that the same deployed account is never
/// returned more than once even if multiple wallet-type derivations produce the
/// same address.
///
/// # Arguments
/// * `mnemonic` - BIP-39 mnemonic phrase
/// * `rpc_url` - Starknet JSON-RPC endpoint URL
/// * `max_index` - Maximum HD derivation index to scan
///
/// # Example
/// ```no_run
/// # async fn example() -> krusty_kms_common::Result<()> {
/// let accounts = krusty_kms_client::discovery::discover_accounts(
///     "person hunt couch artefact ...",
///     "https://starknet-mainnet.g.alchemy.com/starknet/version/rpc/v0_7/YOUR_KEY",
///     5,
/// ).await?;
/// for acct in &accounts {
///     println!("{:?}: {} ({})", acct.wallet_type, acct.address, acct.class_version);
/// }
/// # Ok(())
/// # }
/// ```
pub async fn discover_accounts(
    mnemonic: &str,
    rpc_url: &str,
    max_index: u32,
) -> Result<Vec<DiscoveredAccount>> {
    let candidates = krusty_kms::discovery::generate_candidates(mnemonic, max_index)?;
    let provider = Arc::new(crate::provider::create_provider(rpc_url)?);

    let mut discovered = Vec::new();
    let mut seen_addresses = HashSet::new();

    // Sequential iteration to stay within RPC rate limits.
    for candidate in &candidates {
        // Deduplicate: skip if we already found this address.
        if seen_addresses.contains(&candidate.address) {
            continue;
        }

        let core_felt = CoreFelt::from_hex(&candidate.address)
            .map_err(|e| KmsError::CryptoError(format!("Invalid candidate address: {}", e)))?;
        let rs_address = core_felt_to_rs(core_felt);

        match provider
            .get_class_hash_at(BlockId::Tag(BlockTag::Latest), rs_address)
            .await
        {
            Ok(class_hash) => {
                let deployed_class_hash = format!("{:#x}", rs_felt_to_core(class_hash));
                seen_addresses.insert(candidate.address.clone());
                discovered.push(DiscoveredAccount::from_candidate(
                    candidate,
                    deployed_class_hash,
                ));
            }
            Err(ref error) if is_contract_not_found(error) => {
                // Not deployed — skip this candidate.
            }
            Err(error) => {
                return Err(KmsError::RpcError(error.to_string()));
            }
        }
    }

    Ok(discovered)
}

/// Check if a single address is deployed and return its class hash.
///
/// Returns `Some(class_hash_hex)` if deployed, `None` if not.
///
/// # Arguments
/// * `rpc_url` - Starknet JSON-RPC endpoint URL
/// * `address` - Hex-encoded Starknet address to check
pub async fn check_account_deployed(rpc_url: &str, address: &str) -> Result<Option<String>> {
    let provider = crate::provider::create_provider(rpc_url)?;

    let core_felt = CoreFelt::from_hex(address)
        .map_err(|e| KmsError::CryptoError(format!("Invalid address: {}", e)))?;
    let rs_address = core_felt_to_rs(core_felt);

    match provider
        .get_class_hash_at(BlockId::Tag(BlockTag::Latest), rs_address)
        .await
    {
        Ok(class_hash) => {
            let hex = format!("{:#x}", rs_felt_to_core(class_hash));
            Ok(Some(hex))
        }
        Err(ref error) if is_contract_not_found(error) => Ok(None),
        Err(error) => Err(KmsError::RpcError(error.to_string())),
    }
}

/// Detect the `ContractNotFound` Starknet error from a provider response.
fn is_contract_not_found(error: &ProviderError) -> bool {
    matches!(
        error,
        ProviderError::StarknetError(StarknetError::ContractNotFound)
    )
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_discovered_account_from_candidate() {
        let candidate = CandidateAccount {
            wallet_type: WalletType::OpenZeppelin,
            class_hash: "0xdeadbeef".to_string(),
            address: "0x1234".to_string(),
            public_key: "0xpub".to_string(),
            private_key: "0xpriv".to_string(),
            derivation_index: 0,
            derivation_path: "m/44'/9004'/0'/0/0".to_string(),
            class_version: "v0.14.0".to_string(),
        };

        let discovered = DiscoveredAccount::from_candidate(&candidate, "0xcafebabe".to_string());

        assert_eq!(discovered.address, "0x1234");
        assert_eq!(discovered.class_hash, "0xdeadbeef");
        assert_eq!(discovered.deployed_class_hash, "0xcafebabe");
        assert_eq!(discovered.derivation_index, 0);
        assert_eq!(discovered.derivation_path, "m/44'/9004'/0'/0/0");
        assert_eq!(discovered.public_key, "0xpub");
        assert_eq!(discovered.private_key, "0xpriv");
        assert_eq!(discovered.class_version, "v0.14.0");
        assert!(matches!(discovered.wallet_type, WalletType::OpenZeppelin));
    }

    #[test]
    fn test_discovered_account_preserves_all_candidate_fields() {
        let candidate = CandidateAccount {
            wallet_type: WalletType::Argent,
            class_hash: "0xaa".to_string(),
            address: "0xbb".to_string(),
            public_key: "0xcc".to_string(),
            private_key: "0xdd".to_string(),
            derivation_index: 3,
            derivation_path: "m/44'/9004'/0'/0/3".to_string(),
            class_version: "v0.4.0".to_string(),
        };

        let discovered = DiscoveredAccount::from_candidate(&candidate, "0xee".to_string());

        // Verify the deployed_class_hash can differ from derivation class_hash.
        assert_ne!(discovered.class_hash, discovered.deployed_class_hash);
        assert_eq!(discovered.class_hash, "0xaa");
        assert_eq!(discovered.deployed_class_hash, "0xee");
    }

    #[tokio::test]
    #[ignore] // requires live RPC
    async fn test_discover_accounts_live() {
        let accounts = discover_accounts(
            "person hunt couch artefact try half produce fatal large raw prison electric",
            "https://starknet-mainnet.g.alchemy.com/starknet/version/rpc/v0_7/demo",
            2,
        )
        .await;
        // Just verify it doesn't panic — actual results depend on on-chain state.
        assert!(accounts.is_ok() || accounts.is_err()); // RPC may fail with demo key
    }

    #[tokio::test]
    #[ignore] // requires live RPC
    async fn test_check_account_deployed_live() {
        let result = check_account_deployed(
            "https://starknet-mainnet.g.alchemy.com/starknet/version/rpc/v0_7/demo",
            "0x0000000000000000000000000000000000000000000000000000000000000001",
        )
        .await;
        // With a demo key, the request may succeed or fail — just check no panic.
        assert!(result.is_ok() || result.is_err());
    }
}
