//! macOS Keychain integration for wallet mnemonic storage.
//!
//! The mnemonic is stored in the **Data Protection Keychain** with biometric
//! access control (Touch ID / Face ID, passcode fallback). Reading the secret
//! triggers a native biometric prompt — no ugly "allow access?" dialog.
//!
//! Legacy Keychain items (created before biometric support) are automatically
//! migrated to the Data Protection Keychain on the next successful `load_mnemonic`.

/// Paired session data returned from keychain: (address, session_key, identity_pubkey, p256_pubkey).
///
/// Using a type alias avoids repeating the complex tuple type across load and
/// parse functions while keeping the fields inline (no extra struct overhead).
pub type PairedSessionData = (String, [u8; 16], [u8; 32], [u8; 65]);

#[cfg(target_os = "macos")]
mod macos {
    use anyhow::{anyhow, Result};
    use core_foundation::base::{CFType, TCFType};
    use core_foundation::boolean::CFBoolean;
    use core_foundation::string::CFString;
    use security_framework::access_control::{ProtectionMode, SecAccessControl};
    use security_framework::passwords::{
        delete_generic_password, generic_password, get_generic_password, set_generic_password,
        set_generic_password_options, AccessControlOptions, PasswordOptions,
    };
    use security_framework_sys::base::errSecItemNotFound;
    use security_framework_sys::item;
    use security_framework_sys::keychain_item::SecItemCopyMatching;

    const SERVICE: &str = "io.parity.host-wallet";
    const ACCOUNT: &str = "master-mnemonic";

    /// Keychain account name for paired wallet session data (AES key + pubkeys).
    const ACCOUNT_PAIRED_SESSION: &str = "paired-session";

    /// Check whether a mnemonic exists in the Keychain without reading the
    /// secret data. Uses `SecItemCopyMatching` with `kSecReturnAttributes`
    /// only — this avoids triggering the macOS authentication prompt.
    pub fn has_mnemonic() -> bool {
        // SAFETY: The CF* constants (kSecClass, kSecAttrService, etc.) are
        // static string literals owned by the Security/CoreFoundation framework
        // and are valid for the process lifetime. `wrap_under_get_rule` does not
        // transfer ownership, so we do not call CFRelease on them. The `result`
        // pointer is either null (item not found) or a retained CF object
        // returned by `SecItemCopyMatching`; if non-null we call `CFRelease`
        // exactly once to balance the implicit +1 retain count.
        unsafe {
            let k_class = CFString::wrap_under_get_rule(item::kSecClass);
            let v_generic = CFType::wrap_under_get_rule(item::kSecClassGenericPassword as _);
            let k_service = CFString::wrap_under_get_rule(item::kSecAttrService);
            let v_service = CFString::new(SERVICE);
            let k_account = CFString::wrap_under_get_rule(item::kSecAttrAccount);
            let v_account = CFString::new(ACCOUNT);
            let k_return = CFString::wrap_under_get_rule(item::kSecReturnAttributes);
            let v_true = CFBoolean::true_value();
            let k_dpk = CFString::wrap_under_get_rule(item::kSecUseDataProtectionKeychain);
            let query = core_foundation::dictionary::CFDictionary::from_CFType_pairs(&[
                (k_class, v_generic),
                (k_service, v_service.as_CFType()),
                (k_account, v_account.as_CFType()),
                (k_return, v_true.as_CFType()),
                (k_dpk, v_true.as_CFType()),
            ]);
            let mut result = std::ptr::null();
            let status = SecItemCopyMatching(query.as_concrete_TypeRef(), &mut result);
            if !result.is_null() {
                core_foundation::base::CFRelease(result);
            }
            if status == 0 {
                return true;
            }
        }
        // Also check legacy keychain (pre-biometric items)
        // SAFETY: Same invariants as above — framework-owned constants are
        // valid for process lifetime; result is CFRelease'd if non-null.
        unsafe {
            let k_class = CFString::wrap_under_get_rule(item::kSecClass);
            let v_generic = CFType::wrap_under_get_rule(item::kSecClassGenericPassword as _);
            let k_service = CFString::wrap_under_get_rule(item::kSecAttrService);
            let v_service = CFString::new(SERVICE);
            let k_account = CFString::wrap_under_get_rule(item::kSecAttrAccount);
            let v_account = CFString::new(ACCOUNT);
            let k_return = CFString::wrap_under_get_rule(item::kSecReturnAttributes);
            let v_true = CFBoolean::true_value();
            let query = core_foundation::dictionary::CFDictionary::from_CFType_pairs(&[
                (k_class, v_generic),
                (k_service, v_service.as_CFType()),
                (k_account, v_account.as_CFType()),
                (k_return, v_true.as_CFType()),
            ]);
            let mut result = std::ptr::null();
            let status = SecItemCopyMatching(query.as_concrete_TypeRef(), &mut result);
            if !result.is_null() {
                core_foundation::base::CFRelease(result);
            }
            status == 0
        }
    }

    /// Build a `PasswordOptions` for the Data Protection Keychain with
    /// biometric access control (Touch ID + passcode fallback).
    fn biometric_options() -> Result<PasswordOptions> {
        let mut opts = PasswordOptions::new_generic_password(SERVICE, ACCOUNT);
        opts.use_protected_keychain();
        let acl = SecAccessControl::create_with_protection(
            Some(ProtectionMode::AccessibleWhenPasscodeSetThisDeviceOnly),
            (AccessControlOptions::BIOMETRY_ANY
                | AccessControlOptions::DEVICE_PASSCODE
                | AccessControlOptions::OR)
                .bits(),
        )
        .map_err(|e| anyhow!("Failed to create SecAccessControl: {e}"))?;
        opts.set_access_control(acl);
        Ok(opts)
    }

    /// Store the mnemonic in the Keychain with biometric access control.
    /// Falls back to the legacy keychain if the Data Protection Keychain
    /// is unavailable (e.g. ad-hoc signed binary without entitlements).
    pub fn store_mnemonic(mnemonic: &str) -> Result<()> {
        // Delete legacy entry if present
        let _ = delete_generic_password(SERVICE, ACCOUNT);
        // Delete Data Protection entry if present
        delete_biometric_entry();
        match biometric_options().and_then(|opts| {
            set_generic_password_options(mnemonic.as_bytes(), opts)
                .map_err(|e| anyhow!("Keychain store failed: {e}"))
        }) {
            Ok(()) => Ok(()),
            Err(e) => {
                log::warn!(
                    "Mnemonic stored WITHOUT biometric protection — \
                     Data Protection Keychain unavailable ({e}). \
                     This typically means the binary lacks the Keychain Sharing \
                     entitlement or is running ad-hoc signed. \
                     Falling back to legacy (unprotected) keychain."
                );
                set_generic_password(SERVICE, ACCOUNT, mnemonic.as_bytes())
                    .map_err(|e| anyhow!("Keychain store failed: {e}"))
            }
        }
    }

    /// Load the mnemonic from the Keychain.
    /// Triggers Touch ID / passcode prompt via the biometric access control.
    ///
    /// If a legacy (non-biometric) item is found, it is automatically migrated
    /// to the Data Protection Keychain with biometric access control.
    pub fn load_mnemonic() -> Result<String> {
        // Try Data Protection Keychain first (biometric item)
        let mut opts = PasswordOptions::new_generic_password(SERVICE, ACCOUNT);
        opts.use_protected_keychain();
        match generic_password(opts) {
            Ok(bytes) => {
                return String::from_utf8(bytes)
                    .map_err(|e| anyhow!("Mnemonic not valid UTF-8: {e}"));
            }
            Err(e) => {
                // Only fall through to legacy if the biometric item doesn't exist.
                // If the user cancelled Touch ID or auth failed, propagate the error.
                if e.code() != errSecItemNotFound {
                    return Err(anyhow!("Keychain authentication failed: {e}"));
                }
            }
        }
        // Fallback: try legacy keychain (pre-biometric item).
        let bytes = security_framework::passwords::get_generic_password(SERVICE, ACCOUNT)
            .map_err(|e| anyhow!("Keychain load failed: {e}"))?;
        let phrase =
            String::from_utf8(bytes).map_err(|e| anyhow!("Mnemonic not valid UTF-8: {e}"))?;

        // Migrate: re-store with biometric access control, delete legacy item.
        if store_mnemonic(&phrase).is_ok() {
            log::info!("Migrated wallet mnemonic to biometric-protected Keychain");
        }
        Ok(phrase)
    }

    /// Delete the mnemonic from both legacy and Data Protection Keychains.
    pub fn delete_mnemonic() -> Result<()> {
        let _ = delete_generic_password(SERVICE, ACCOUNT);
        delete_biometric_entry();
        Ok(())
    }

    // -----------------------------------------------------------------------
    // Per-ID mnemonic storage (KeyRing support)
    // -----------------------------------------------------------------------

    /// Store a mnemonic under a specific account ID with biometric protection.
    ///
    /// Uses account name `"mnemonic:<id>"`. Falls back to unprotected keychain
    /// if the Data Protection Keychain is unavailable (e.g. ad-hoc signing).
    ///
    /// # C-1: iCloud sync vs biometric protection
    ///
    /// Apple does not allow `kSecAttrSynchronizable` with biometric
    /// `SecAccessControl` (the call is rejected). When `sync = true`:
    /// - Biometric ACL is **omitted** — the item is protected only by the
    ///   device passcode (`AccessibleWhenUnlocked`).
    /// - `kSecAttrSynchronizable = true` enables iCloud Keychain sync.
    ///
    /// When `sync = false`:
    /// - Full biometric ACL (Touch ID / Face ID + passcode fallback).
    /// - `AccessibleWhenPasscodeSetThisDeviceOnly` — no sync.
    ///
    /// Callers must be aware of this tradeoff: enabling iCloud sync means
    /// the mnemonic is accessible without biometric authentication.
    pub fn store_mnemonic_for_id(id: &str, phrase: &str, sync: bool) -> Result<()> {
        let account = format!("mnemonic:{id}");
        // Remove any existing entry for this ID first.
        let _ = delete_generic_password(SERVICE, &account);
        delete_biometric_entry_for_account(&account);

        let mut opts = PasswordOptions::new_generic_password(SERVICE, &account);
        opts.use_protected_keychain();

        if sync {
            // C-1 fix: iCloud sync is incompatible with biometric ACL.
            // Use AccessibleWhenUnlocked without biometric constraints.
            opts.set_access_synchronized(Some(true));
        } else {
            // Device-only: full biometric protection.
            let acl = SecAccessControl::create_with_protection(
                Some(ProtectionMode::AccessibleWhenPasscodeSetThisDeviceOnly),
                (AccessControlOptions::BIOMETRY_ANY
                    | AccessControlOptions::DEVICE_PASSCODE
                    | AccessControlOptions::OR)
                    .bits(),
            )
            .map_err(|e| anyhow!("Failed to create SecAccessControl: {e}"))?;
            opts.set_access_control(acl);
        }

        if sync {
            // Synced items must not silently fall back to unprotected storage.
            set_generic_password_options(phrase.as_bytes(), opts)
                .map_err(|e| anyhow!("Keychain store (synced) failed: {e}"))
        } else {
            // Device-only: fall back to legacy keychain if Data Protection
            // Keychain is unavailable (e.g. ad-hoc signing on CI).
            match set_generic_password_options(phrase.as_bytes(), opts)
                .map_err(|e| anyhow!("Keychain store failed: {e}"))
            {
                Ok(()) => Ok(()),
                Err(e) => {
                    log::warn!(
                        "Mnemonic for id '{id}' stored WITHOUT biometric protection ({e}). \
                         Falling back to legacy keychain."
                    );
                    set_generic_password(SERVICE, &account, phrase.as_bytes())
                        .map_err(|e| anyhow!("Keychain store failed: {e}"))
                }
            }
        }
    }

    /// Load a mnemonic by account ID. Triggers Touch ID / passcode prompt.
    pub fn load_mnemonic_for_id(id: &str) -> Result<Option<zeroize::Zeroizing<String>>> {
        let account = format!("mnemonic:{id}");
        let mut opts = PasswordOptions::new_generic_password(SERVICE, &account);
        opts.use_protected_keychain();

        match generic_password(opts) {
            Ok(bytes) => {
                let phrase = String::from_utf8(bytes)
                    .map_err(|e| anyhow!("Mnemonic not valid UTF-8: {e}"))?;
                return Ok(Some(zeroize::Zeroizing::new(phrase)));
            }
            Err(e) if e.code() == security_framework_sys::base::errSecItemNotFound => {}
            Err(e) => return Err(anyhow!("Keychain authentication failed: {e}")),
        }

        // Fallback: legacy (non-biometric) item.
        match get_generic_password(SERVICE, &account) {
            Ok(bytes) => {
                let phrase = String::from_utf8(bytes)
                    .map_err(|e| anyhow!("Mnemonic not valid UTF-8: {e}"))?;
                Ok(Some(zeroize::Zeroizing::new(phrase)))
            }
            Err(e) if e.code() == security_framework_sys::base::errSecItemNotFound => Ok(None),
            Err(e) => Err(anyhow!("Keychain load failed: {e}")),
        }
    }

    /// Delete a mnemonic by account ID from both keychains.
    pub fn delete_mnemonic_for_id(id: &str) -> Result<()> {
        let account = format!("mnemonic:{id}");
        let _ = delete_generic_password(SERVICE, &account);
        delete_biometric_entry_for_account(&account);
        Ok(())
    }

    /// Store the keyring index (JSON metadata, no secrets) without biometric.
    ///
    /// The index contains only public data (addresses, names, derivation paths).
    /// When `sync = true`, `kSecAttrSynchronizable` is set so the index is
    /// available on other iCloud-connected devices (C-2 fix).
    pub fn store_keyring_index(json: &str, sync: bool) -> Result<()> {
        let _ = delete_generic_password(SERVICE, "keyring-index");
        if sync {
            let mut opts = PasswordOptions::new_generic_password(SERVICE, "keyring-index");
            opts.use_protected_keychain();
            opts.set_access_synchronized(Some(true));
            set_generic_password_options(json.as_bytes(), opts)
                .map_err(|e| anyhow!("Keychain store of keyring index (synced) failed: {e}"))
        } else {
            set_generic_password(SERVICE, "keyring-index", json.as_bytes())
                .map_err(|e| anyhow!("Keychain store of keyring index failed: {e}"))
        }
    }

    /// Load the keyring index. Returns `None` if not yet stored.
    pub fn load_keyring_index() -> Option<String> {
        let bytes = get_generic_password(SERVICE, "keyring-index").ok()?;
        String::from_utf8(bytes).ok()
    }

    /// Delete a biometric-protected entry for an arbitrary account name.
    fn delete_biometric_entry_for_account(account: &str) {
        use core_foundation::dictionary::CFDictionary;
        use security_framework_sys::keychain_item::SecItemDelete;
        // SAFETY: Framework-owned CF constants are valid for process lifetime.
        // The dictionary is stack-allocated and outlives the SecItemDelete call.
        // The OSStatus return value is intentionally ignored — errSecItemNotFound
        // is benign when the item was never stored.
        unsafe {
            let k_class = CFString::wrap_under_get_rule(item::kSecClass);
            let v_generic = CFType::wrap_under_get_rule(item::kSecClassGenericPassword as _);
            let k_service = CFString::wrap_under_get_rule(item::kSecAttrService);
            let v_service = CFString::new(SERVICE);
            let k_account = CFString::wrap_under_get_rule(item::kSecAttrAccount);
            let v_account = CFString::new(account);
            let k_dpk = CFString::wrap_under_get_rule(item::kSecUseDataProtectionKeychain);
            let v_true = CFBoolean::true_value();
            let query = CFDictionary::from_CFType_pairs(&[
                (k_class, v_generic),
                (k_service, v_service.as_CFType()),
                (k_account, v_account.as_CFType()),
                (k_dpk, v_true.as_CFType()),
            ]);
            SecItemDelete(query.as_concrete_TypeRef());
        }
    }

    // -----------------------------------------------------------------------
    // Paired wallet session data
    // -----------------------------------------------------------------------
    //
    // All four fields (address, AES session key, sr25519 identity pubkey, P-256
    // pubkey) are stored together as a single hex-encoded JSON blob under
    // ACCOUNT_PAIRED_SESSION in the legacy (non-biometric) keychain.
    //
    // Audit C4 — fixed: paired session key now uses biometric ACL with
    // BIOMETRY_CURRENT_SET (invalidates on enrollment change) + DEVICE_PASSCODE
    // fallback. Migration from legacy keychain is transparent on first load.

    /// Build biometric PasswordOptions for the paired session key.
    /// Uses BIOMETRY_CURRENT_SET (stricter than mnemonic's BIOMETRY_ANY) —
    /// the item is invalidated if Touch ID enrollment changes.
    fn paired_biometric_options() -> Result<PasswordOptions> {
        let mut opts = PasswordOptions::new_generic_password(SERVICE, ACCOUNT_PAIRED_SESSION);
        opts.use_protected_keychain();
        let acl = SecAccessControl::create_with_protection(
            Some(ProtectionMode::AccessibleWhenPasscodeSetThisDeviceOnly),
            (AccessControlOptions::BIOMETRY_CURRENT_SET
                | AccessControlOptions::DEVICE_PASSCODE
                | AccessControlOptions::OR)
                .bits(),
        )
        .map_err(|e| anyhow!("Failed to create SecAccessControl for paired data: {e}"))?;
        opts.set_access_control(acl);
        Ok(opts)
    }

    /// Store paired wallet data in the Keychain with biometric protection.
    /// Falls back to the legacy keychain if biometric ACL is unavailable
    /// (e.g. no passcode set, ad-hoc signed binary on CI).
    pub fn store_paired_data(
        address: &str,
        session_key: &[u8; 16],
        identity_pubkey: &[u8; 32],
        p256_pubkey: &[u8; 65],
    ) -> Result<()> {
        let blob = serde_json::json!({
            "address": address,
            "session_key": hex::encode(session_key),
            "identity_pubkey": hex::encode(identity_pubkey),
            "p256_pubkey": hex::encode(p256_pubkey),
        });
        let blob_str = serde_json::to_string(&blob)
            .map_err(|e| anyhow!("Failed to serialize paired data: {e}"))?;

        // Clear any existing entry in both keychains
        let _ = delete_generic_password(SERVICE, ACCOUNT_PAIRED_SESSION);
        delete_biometric_entry_for_account(ACCOUNT_PAIRED_SESSION);

        // Try biometric-protected storage first
        if let Ok(opts) = paired_biometric_options() {
            if set_generic_password_options(blob_str.as_bytes(), opts).is_ok() {
                return Ok(());
            }
        }

        // Fallback: legacy keychain (no biometric, matches pre-migration behavior)
        log::warn!(
            "[keystore] biometric paired-data store failed, falling back to legacy keychain"
        );
        set_generic_password(SERVICE, ACCOUNT_PAIRED_SESSION, blob_str.as_bytes())
            .map_err(|e| anyhow!("Keychain store of paired data failed: {e}"))
    }

    /// Load paired wallet data from the Keychain.
    /// Checks biometric-protected (Data Protection) keychain first, then
    /// falls back to legacy. On successful legacy load, migrates the item
    /// to biometric storage transparently.
    pub fn load_paired_data() -> Option<super::PairedSessionData> {
        // Try Data Protection Keychain (biometric) first.
        // Only fall through to legacy on errSecItemNotFound — auth failures
        // (user cancel, biometric mismatch) must NOT bypass the biometric gate.
        if let Ok(opts) = paired_biometric_options() {
            match generic_password(opts) {
                Ok(bytes) => {
                    let blob: serde_json::Value = serde_json::from_slice(&bytes).ok()?;
                    return super::parse_paired_blob(&blob);
                }
                Err(e) if e.code() == security_framework_sys::base::errSecItemNotFound => {
                    // Item not in Data Protection Keychain — fall through to legacy
                }
                Err(_) => {
                    // Auth failure or user cancel — do not bypass biometric gate
                    return None;
                }
            }
        }

        // Fallback: legacy keychain (item not yet migrated)
        let bytes = get_generic_password(SERVICE, ACCOUNT_PAIRED_SESSION).ok()?;
        let blob: serde_json::Value = serde_json::from_slice(&bytes).ok()?;
        let data = super::parse_paired_blob(&blob)?;

        // Migrate: re-store with biometric ACL
        if store_paired_data(&data.0, &data.1, &data.2, &data.3).is_ok() {
            log::info!("[keystore] migrated paired session key to biometric-protected storage");
        } else {
            log::warn!("[keystore] failed to migrate paired session key to biometric storage");
        }

        Some(data)
    }

    /// Returns `true` if paired wallet data exists in either keychain.
    pub fn has_paired_wallet() -> bool {
        // Check Data Protection Keychain first
        if item_exists_in_data_protection_keychain(ACCOUNT_PAIRED_SESSION) {
            return true;
        }
        // Fallback: check legacy keychain
        item_exists_in_legacy_keychain(ACCOUNT_PAIRED_SESSION)
    }

    /// Delete all paired wallet data from both keychains.
    pub fn delete_paired_data() -> Result<()> {
        let _ = delete_generic_password(SERVICE, ACCOUNT_PAIRED_SESSION);
        delete_biometric_entry_for_account(ACCOUNT_PAIRED_SESSION);
        Ok(())
    }

    fn item_exists_in_data_protection_keychain(account: &str) -> bool {
        unsafe {
            let k_class = CFString::wrap_under_get_rule(item::kSecClass);
            let v_generic = CFType::wrap_under_get_rule(item::kSecClassGenericPassword as _);
            let k_service = CFString::wrap_under_get_rule(item::kSecAttrService);
            let v_service = CFString::new(SERVICE);
            let k_account = CFString::wrap_under_get_rule(item::kSecAttrAccount);
            let v_account = CFString::new(account);
            let k_return = CFString::wrap_under_get_rule(item::kSecReturnAttributes);
            let k_dpk = CFString::wrap_under_get_rule(item::kSecUseDataProtectionKeychain);
            let v_true = CFBoolean::true_value();
            let query = core_foundation::dictionary::CFDictionary::from_CFType_pairs(&[
                (k_class, v_generic),
                (k_service, v_service.as_CFType()),
                (k_account, v_account.as_CFType()),
                (k_return, v_true.as_CFType()),
                (k_dpk, v_true.as_CFType()),
            ]);
            let mut result = std::ptr::null();
            let status = SecItemCopyMatching(query.as_concrete_TypeRef(), &mut result);
            if !result.is_null() {
                core_foundation::base::CFRelease(result);
            }
            status == 0
        }
    }

    fn item_exists_in_legacy_keychain(account: &str) -> bool {
        unsafe {
            let k_class = CFString::wrap_under_get_rule(item::kSecClass);
            let v_generic = CFType::wrap_under_get_rule(item::kSecClassGenericPassword as _);
            let k_service = CFString::wrap_under_get_rule(item::kSecAttrService);
            let v_service = CFString::new(SERVICE);
            let k_account = CFString::wrap_under_get_rule(item::kSecAttrAccount);
            let v_account = CFString::new(account);
            let k_return = CFString::wrap_under_get_rule(item::kSecReturnAttributes);
            let v_true = CFBoolean::true_value();
            let query = core_foundation::dictionary::CFDictionary::from_CFType_pairs(&[
                (k_class, v_generic),
                (k_service, v_service.as_CFType()),
                (k_account, v_account.as_CFType()),
                (k_return, v_true.as_CFType()),
            ]);
            let mut result = std::ptr::null();
            let status = SecItemCopyMatching(query.as_concrete_TypeRef(), &mut result);
            if !result.is_null() {
                core_foundation::base::CFRelease(result);
            }
            status == 0
        }
    }

    /// Delete the biometric-protected entry from the Data Protection Keychain.
    fn delete_biometric_entry() {
        use core_foundation::dictionary::CFDictionary;
        use security_framework_sys::keychain_item::SecItemDelete;
        // SAFETY: Framework-owned CF constants are valid for process lifetime.
        // `SecItemDelete` takes a borrowed CFDictionaryRef; the dictionary is
        // kept alive on the stack for the duration of the call. The return
        // value (OSStatus) is intentionally ignored — errSecItemNotFound is
        // benign when the item was never stored.
        unsafe {
            let k_class = CFString::wrap_under_get_rule(item::kSecClass);
            let v_generic = CFType::wrap_under_get_rule(item::kSecClassGenericPassword as _);
            let k_service = CFString::wrap_under_get_rule(item::kSecAttrService);
            let v_service = CFString::new(SERVICE);
            let k_account = CFString::wrap_under_get_rule(item::kSecAttrAccount);
            let v_account = CFString::new(ACCOUNT);
            let k_dpk = CFString::wrap_under_get_rule(item::kSecUseDataProtectionKeychain);
            let v_true = CFBoolean::true_value();
            let query = CFDictionary::from_CFType_pairs(&[
                (k_class, v_generic),
                (k_service, v_service.as_CFType()),
                (k_account, v_account.as_CFType()),
                (k_dpk, v_true.as_CFType()),
            ]);
            SecItemDelete(query.as_concrete_TypeRef());
        }
    }
}

#[cfg(target_os = "macos")]
pub use macos::*;

// ---------------------------------------------------------------------------
// iOS — Keychain + Face ID (biometric access control)
// ---------------------------------------------------------------------------
//
// Structurally identical to the macOS backend but without:
// - `kSecUseDataProtectionKeychain` (iOS always uses data protection)
// - Legacy keychain migration (not applicable on iOS)
// - `use_protected_keychain()` on PasswordOptions (macOS-only API)

#[cfg(target_os = "ios")]
mod ios {
    use anyhow::{anyhow, Result};
    use core_foundation::base::{CFType, TCFType};
    use core_foundation::boolean::CFBoolean;
    use core_foundation::string::CFString;
    use security_framework::access_control::{ProtectionMode, SecAccessControl};
    use security_framework::passwords::{
        delete_generic_password, generic_password, get_generic_password, set_generic_password,
        set_generic_password_options, AccessControlOptions, PasswordOptions,
    };
    use security_framework_sys::item;
    use security_framework_sys::keychain_item::SecItemCopyMatching;

    const SERVICE: &str = "io.parity.host-wallet";
    const ACCOUNT: &str = "master-mnemonic";
    const ACCOUNT_PAIRED_SESSION: &str = "paired-session";

    /// Check whether a mnemonic exists in the Keychain without triggering
    /// Face ID. Uses `SecItemCopyMatching` with `kSecReturnAttributes` only.
    pub fn has_mnemonic() -> bool {
        item_exists(ACCOUNT)
    }

    /// Build `PasswordOptions` with Face ID + passcode biometric access control.
    fn biometric_options() -> Result<PasswordOptions> {
        let mut opts = PasswordOptions::new_generic_password(SERVICE, ACCOUNT);
        let acl = SecAccessControl::create_with_protection(
            Some(ProtectionMode::AccessibleWhenPasscodeSetThisDeviceOnly),
            (AccessControlOptions::BIOMETRY_ANY
                | AccessControlOptions::DEVICE_PASSCODE
                | AccessControlOptions::OR)
                .bits(),
        )
        .map_err(|e| anyhow!("Failed to create SecAccessControl: {e}"))?;
        opts.set_access_control(acl);
        Ok(opts)
    }

    /// Store the mnemonic in the iOS Keychain with Face ID access control.
    pub fn store_mnemonic(mnemonic: &str) -> Result<()> {
        let _ = delete_generic_password(SERVICE, ACCOUNT);
        let opts = biometric_options()?;
        set_generic_password_options(mnemonic.as_bytes(), opts)
            .map_err(|e| anyhow!("Keychain store failed: {e}"))
    }

    /// Load the mnemonic from the iOS Keychain.
    /// Triggers Face ID / passcode prompt via the biometric access control.
    pub fn load_mnemonic() -> Result<String> {
        let opts = PasswordOptions::new_generic_password(SERVICE, ACCOUNT);
        match generic_password(opts) {
            Ok(bytes) => {
                String::from_utf8(bytes).map_err(|e| anyhow!("Mnemonic not valid UTF-8: {e}"))
            }
            Err(e) => Err(anyhow!("Keychain authentication failed: {e}")),
        }
    }

    /// Delete the mnemonic from the iOS Keychain.
    pub fn delete_mnemonic() -> Result<()> {
        let _ = delete_generic_password(SERVICE, ACCOUNT);
        Ok(())
    }

    // -----------------------------------------------------------------------
    // Per-ID mnemonic storage (KeyRing support)
    // -----------------------------------------------------------------------

    /// Store a mnemonic under a specific account ID with Face ID protection.
    ///
    /// # C-1: iCloud sync vs biometric protection
    ///
    /// Apple does not allow `kSecAttrSynchronizable` with biometric
    /// `SecAccessControl` (the call is rejected). When `sync = true`:
    /// - Biometric ACL is **omitted** — the item is protected only by the
    ///   device passcode (`AccessibleWhenUnlocked`).
    /// - `kSecAttrSynchronizable = true` enables iCloud Keychain sync.
    ///
    /// When `sync = false`:
    /// - Full biometric ACL (Face ID + passcode fallback).
    /// - `AccessibleWhenPasscodeSetThisDeviceOnly` — no sync.
    pub fn store_mnemonic_for_id(id: &str, phrase: &str, sync: bool) -> Result<()> {
        let account = format!("mnemonic:{id}");
        let _ = delete_generic_password(SERVICE, &account);

        let mut opts = PasswordOptions::new_generic_password(SERVICE, &account);

        if sync {
            // C-1 fix: iCloud sync is incompatible with biometric ACL.
            // Use AccessibleWhenUnlocked without biometric constraints.
            opts.set_access_synchronized(Some(true));
        } else {
            // Device-only: full biometric protection.
            let acl = SecAccessControl::create_with_protection(
                Some(ProtectionMode::AccessibleWhenPasscodeSetThisDeviceOnly),
                (AccessControlOptions::BIOMETRY_ANY
                    | AccessControlOptions::DEVICE_PASSCODE
                    | AccessControlOptions::OR)
                    .bits(),
            )
            .map_err(|e| anyhow!("Failed to create SecAccessControl: {e}"))?;
            opts.set_access_control(acl);
        }

        set_generic_password_options(phrase.as_bytes(), opts)
            .map_err(|e| anyhow!("Keychain store failed: {e}"))
    }

    /// Load a mnemonic by account ID. Triggers Face ID / passcode prompt.
    pub fn load_mnemonic_for_id(id: &str) -> Result<Option<zeroize::Zeroizing<String>>> {
        let account = format!("mnemonic:{id}");
        let opts = PasswordOptions::new_generic_password(SERVICE, &account);
        match generic_password(opts) {
            Ok(bytes) => {
                let phrase = String::from_utf8(bytes)
                    .map_err(|e| anyhow!("Mnemonic not valid UTF-8: {e}"))?;
                Ok(Some(zeroize::Zeroizing::new(phrase)))
            }
            Err(e) => {
                use security_framework_sys::base::errSecItemNotFound;
                if e.code() == errSecItemNotFound {
                    Ok(None)
                } else {
                    Err(anyhow!("Keychain authentication failed: {e}"))
                }
            }
        }
    }

    /// Delete a mnemonic by account ID from the iOS Keychain.
    pub fn delete_mnemonic_for_id(id: &str) -> Result<()> {
        let account = format!("mnemonic:{id}");
        let _ = delete_generic_password(SERVICE, &account);
        Ok(())
    }

    /// Store the keyring index (JSON metadata, no secrets) without biometric.
    ///
    /// When `sync = true`, `kSecAttrSynchronizable` is set so the index is
    /// available on other iCloud-connected devices (C-2 fix).
    pub fn store_keyring_index(json: &str, sync: bool) -> Result<()> {
        let _ = delete_generic_password(SERVICE, "keyring-index");
        if sync {
            let mut opts = PasswordOptions::new_generic_password(SERVICE, "keyring-index");
            opts.set_access_synchronized(Some(true));
            set_generic_password_options(json.as_bytes(), opts)
                .map_err(|e| anyhow!("Keychain store of keyring index (synced) failed: {e}"))
        } else {
            set_generic_password(SERVICE, "keyring-index", json.as_bytes())
                .map_err(|e| anyhow!("Keychain store of keyring index failed: {e}"))
        }
    }

    /// Load the keyring index. Returns `None` if not yet stored.
    pub fn load_keyring_index() -> Option<String> {
        let bytes = get_generic_password(SERVICE, "keyring-index").ok()?;
        String::from_utf8(bytes).ok()
    }

    /// Store paired wallet data in the Keychain (non-biometric).
    pub fn store_paired_data(
        address: &str,
        session_key: &[u8; 16],
        identity_pubkey: &[u8; 32],
        p256_pubkey: &[u8; 65],
    ) -> Result<()> {
        let blob = serde_json::json!({
            "address": address,
            "session_key": hex::encode(session_key),
            "identity_pubkey": hex::encode(identity_pubkey),
            "p256_pubkey": hex::encode(p256_pubkey),
        });
        let blob_str = serde_json::to_string(&blob)
            .map_err(|e| anyhow!("Failed to serialize paired data: {e}"))?;
        let _ = delete_generic_password(SERVICE, ACCOUNT_PAIRED_SESSION);
        set_generic_password(SERVICE, ACCOUNT_PAIRED_SESSION, blob_str.as_bytes())
            .map_err(|e| anyhow!("Keychain store of paired data failed: {e}"))
    }

    /// Load paired wallet data from the Keychain.
    pub fn load_paired_data() -> Option<super::PairedSessionData> {
        let bytes = get_generic_password(SERVICE, ACCOUNT_PAIRED_SESSION).ok()?;
        let blob: serde_json::Value = serde_json::from_slice(&bytes).ok()?;
        super::parse_paired_blob(&blob)
    }

    /// Returns `true` if paired wallet data exists in the Keychain.
    pub fn has_paired_wallet() -> bool {
        item_exists(ACCOUNT_PAIRED_SESSION)
    }

    /// Delete all paired wallet data from the Keychain.
    pub fn delete_paired_data() -> Result<()> {
        let _ = delete_generic_password(SERVICE, ACCOUNT_PAIRED_SESSION);
        Ok(())
    }

    /// Check if a Keychain item exists without reading its data or triggering
    /// biometric authentication.
    fn item_exists(account: &str) -> bool {
        // SAFETY: CF constants are framework-owned, valid for process lifetime.
        // `result` is CFRelease'd if non-null to balance the retain count.
        unsafe {
            let k_class = CFString::wrap_under_get_rule(item::kSecClass);
            let v_generic = CFType::wrap_under_get_rule(item::kSecClassGenericPassword as _);
            let k_service = CFString::wrap_under_get_rule(item::kSecAttrService);
            let v_service = CFString::new(SERVICE);
            let k_account = CFString::wrap_under_get_rule(item::kSecAttrAccount);
            let v_account = CFString::new(account);
            let k_return = CFString::wrap_under_get_rule(item::kSecReturnAttributes);
            let v_true = CFBoolean::true_value();
            let query = core_foundation::dictionary::CFDictionary::from_CFType_pairs(&[
                (k_class, v_generic),
                (k_service, v_service.as_CFType()),
                (k_account, v_account.as_CFType()),
                (k_return, v_true.as_CFType()),
            ]);
            let mut result = std::ptr::null();
            let status = SecItemCopyMatching(query.as_concrete_TypeRef(), &mut result);
            if !result.is_null() {
                core_foundation::base::CFRelease(result);
            }
            status == 0
        }
    }
}

#[cfg(target_os = "ios")]
pub use ios::*;

// ---------------------------------------------------------------------------
// Shared helpers used by both macos, ios, and desktop modules
// ---------------------------------------------------------------------------

/// Parse a paired-session JSON blob (shared by macOS and desktop backends).
///
/// Expects keys `"address"`, `"session_key"` (hex, 16 bytes),
/// `"identity_pubkey"` (hex, 32 bytes), and `"p256_pubkey"` (hex, 65 bytes).
/// Returns `None` on any parse or length error.
#[cfg(not(target_arch = "wasm32"))]
fn parse_paired_blob(blob: &serde_json::Value) -> Option<PairedSessionData> {
    let address = blob.get("address")?.as_str()?.to_string();

    let session_bytes = hex::decode(blob.get("session_key")?.as_str()?).ok()?;
    if session_bytes.len() != 16 {
        return None;
    }
    let mut session_key = [0u8; 16];
    session_key.copy_from_slice(&session_bytes);

    let identity_bytes = hex::decode(blob.get("identity_pubkey")?.as_str()?).ok()?;
    if identity_bytes.len() != 32 {
        return None;
    }
    let mut identity_pubkey = [0u8; 32];
    identity_pubkey.copy_from_slice(&identity_bytes);

    let p256_bytes = hex::decode(blob.get("p256_pubkey")?.as_str()?).ok()?;
    if p256_bytes.len() != 65 {
        return None;
    }
    let mut p256_pubkey = [0u8; 65];
    p256_pubkey.copy_from_slice(&p256_bytes);

    Some((address, session_key, identity_pubkey, p256_pubkey))
}

// ---------------------------------------------------------------------------
// Desktop (Linux, Windows) — keyring-based credential storage
// ---------------------------------------------------------------------------

#[cfg(all(
    not(target_os = "macos"),
    not(target_os = "ios"),
    not(target_os = "android"),
    not(target_arch = "wasm32")
))]
mod desktop {
    use anyhow::{anyhow, Result};

    const SERVICE: &str = "io.parity.host-wallet";
    const ACCOUNT_MNEMONIC: &str = "master-mnemonic";
    const ACCOUNT_PAIRED_SESSION: &str = "paired-session";

    /// Check whether a mnemonic exists in the credential store.
    ///
    /// On Linux this reads the credential via D-Bus (Secret Service protocol),
    /// which involves a round-trip to the secrets daemon (e.g. gnome-keyring).
    /// This is safe at construction time (`WalletManager::new`) but must not
    /// be called on every render frame or in a tight poll loop — use the
    /// cached `WalletManager::has_mnemonic` field for repeated reads instead.
    pub fn has_mnemonic() -> bool {
        keyring::Entry::new(SERVICE, ACCOUNT_MNEMONIC)
            .ok()
            .and_then(|e| e.get_password().ok())
            .is_some()
    }

    pub fn store_mnemonic(mnemonic: &str) -> Result<()> {
        let entry = keyring::Entry::new(SERVICE, ACCOUNT_MNEMONIC)
            .map_err(|e| anyhow!("keyring init failed: {e}"))?;
        // Delete existing entry first to avoid "already exists" errors.
        let _ = entry.delete_credential();
        entry
            .set_password(mnemonic)
            .map_err(|e| anyhow!("keyring store failed: {e}"))
    }

    pub fn load_mnemonic() -> Result<String> {
        let entry = keyring::Entry::new(SERVICE, ACCOUNT_MNEMONIC)
            .map_err(|e| anyhow!("keyring init failed: {e}"))?;
        entry
            .get_password()
            .map_err(|e| anyhow!("keyring load failed: {e}"))
    }

    pub fn delete_mnemonic() -> Result<()> {
        let entry = keyring::Entry::new(SERVICE, ACCOUNT_MNEMONIC)
            .map_err(|e| anyhow!("keyring init failed: {e}"))?;
        match entry.delete_credential() {
            Ok(()) | Err(keyring::Error::NoEntry) => Ok(()),
            Err(e) => Err(anyhow!("keyring delete failed: {e}")),
        }
    }

    // -----------------------------------------------------------------------
    // Per-ID mnemonic storage (KeyRing support)
    // -----------------------------------------------------------------------

    /// Store a mnemonic under a specific account ID.
    ///
    /// The `sync` parameter is accepted for API parity with the Apple backends
    /// but is ignored on Linux/Windows — no iCloud support on these platforms.
    pub fn store_mnemonic_for_id(id: &str, phrase: &str, _sync: bool) -> Result<()> {
        let account = format!("mnemonic:{id}");
        let entry = keyring::Entry::new(SERVICE, &account)
            .map_err(|e| anyhow!("keyring init failed: {e}"))?;
        let _ = entry.delete_credential();
        entry
            .set_password(phrase)
            .map_err(|e| anyhow!("keyring store failed: {e}"))
    }

    /// Load a mnemonic by account ID.
    pub fn load_mnemonic_for_id(id: &str) -> Result<Option<zeroize::Zeroizing<String>>> {
        let account = format!("mnemonic:{id}");
        let entry = keyring::Entry::new(SERVICE, &account)
            .map_err(|e| anyhow!("keyring init failed: {e}"))?;
        match entry.get_password() {
            Ok(phrase) => Ok(Some(zeroize::Zeroizing::new(phrase))),
            Err(keyring::Error::NoEntry) => Ok(None),
            Err(e) => Err(anyhow!("keyring load failed: {e}")),
        }
    }

    /// Delete a mnemonic by account ID.
    pub fn delete_mnemonic_for_id(id: &str) -> Result<()> {
        let account = format!("mnemonic:{id}");
        let entry = keyring::Entry::new(SERVICE, &account)
            .map_err(|e| anyhow!("keyring init failed: {e}"))?;
        match entry.delete_credential() {
            Ok(()) | Err(keyring::Error::NoEntry) => Ok(()),
            Err(e) => Err(anyhow!("keyring delete failed: {e}")),
        }
    }

    /// Store the keyring index (JSON metadata, no secrets).
    ///
    /// `sync` is accepted for API parity but ignored on non-Apple desktop
    /// platforms (no iCloud Keychain equivalent).
    pub fn store_keyring_index(json: &str, _sync: bool) -> Result<()> {
        let entry = keyring::Entry::new(SERVICE, "keyring-index")
            .map_err(|e| anyhow!("keyring init failed: {e}"))?;
        let _ = entry.delete_credential();
        entry
            .set_password(json)
            .map_err(|e| anyhow!("keyring store of keyring index failed: {e}"))
    }

    /// Load the keyring index. Returns `None` if not yet stored.
    pub fn load_keyring_index() -> Option<String> {
        let entry = keyring::Entry::new(SERVICE, "keyring-index").ok()?;
        entry.get_password().ok()
    }

    pub fn store_paired_data(
        address: &str,
        session_key: &[u8; 16],
        identity_pubkey: &[u8; 32],
        p256_pubkey: &[u8; 65],
    ) -> Result<()> {
        let blob = serde_json::json!({
            "address": address,
            "session_key": hex::encode(session_key),
            "identity_pubkey": hex::encode(identity_pubkey),
            "p256_pubkey": hex::encode(p256_pubkey),
        });
        let blob_str = serde_json::to_string(&blob)
            .map_err(|e| anyhow!("Failed to serialize paired data: {e}"))?;

        let entry = keyring::Entry::new(SERVICE, ACCOUNT_PAIRED_SESSION)
            .map_err(|e| anyhow!("keyring init failed: {e}"))?;
        let _ = entry.delete_credential();
        entry
            .set_password(&blob_str)
            .map_err(|e| anyhow!("keyring store of paired data failed: {e}"))
    }

    pub fn load_paired_data() -> Option<super::PairedSessionData> {
        let entry = keyring::Entry::new(SERVICE, ACCOUNT_PAIRED_SESSION).ok()?;
        let blob_str = entry.get_password().ok()?;
        let blob: serde_json::Value = serde_json::from_str(&blob_str).ok()?;
        super::parse_paired_blob(&blob)
    }

    pub fn has_paired_wallet() -> bool {
        keyring::Entry::new(SERVICE, ACCOUNT_PAIRED_SESSION)
            .ok()
            .and_then(|e| e.get_password().ok())
            .is_some()
    }

    pub fn delete_paired_data() -> Result<()> {
        let entry = keyring::Entry::new(SERVICE, ACCOUNT_PAIRED_SESSION)
            .map_err(|e| anyhow!("keyring init failed: {e}"))?;
        match entry.delete_credential() {
            Ok(()) | Err(keyring::Error::NoEntry) => Ok(()),
            Err(e) => Err(anyhow!("keyring delete failed: {e}")),
        }
    }

    // -----------------------------------------------------------------------
    // Desktop keystore tests
    //
    // These tests activate the keyring mock credential builder, which stores
    // credentials in-memory inside each `Entry` object.  Because the mock uses
    // `CredentialPersistence::EntryOnly`, each `Entry::new()` call returns a
    // fresh, independent credential — there is no shared backing store across
    // separate `Entry` instances.
    //
    // Consequence: round-trip tests (store via one Entry, load via another
    // Entry for the same service/user) do NOT work with the mock — `load_*`
    // functions always construct a new Entry and therefore see an empty store.
    // Those tests are marked `#[ignore]`; run them against a real credential
    // daemon (gnome-keyring / KWallet on Linux, Credential Manager on Windows)
    // with `cargo test -p host-wallet -- --ignored`.
    //
    // Tests that exercise a single operation in isolation (has_mnemonic →
    // returns false, delete is idempotent, store succeeds without error) work
    // correctly with the mock and run in all CI environments without any
    // system credential daemon.
    // -----------------------------------------------------------------------

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

        /// Install the keyring mock credential builder once per test binary.
        ///
        /// `set_default_credential_builder` acquires a write lock on a global
        /// static.  Calling it from multiple threads simultaneously is safe
        /// (the function itself is thread-safe), but we use `OnceLock` to
        /// ensure we only pay the lock cost once and avoid redundant writes.
        fn setup_mock() {
            static ONCE: std::sync::OnceLock<()> = std::sync::OnceLock::new();
            ONCE.get_or_init(|| {
                keyring::set_default_credential_builder(keyring::mock::default_credential_builder());
            });
        }

        #[test]
        fn test_has_mnemonic_returns_false_initially() {
            setup_mock();
            // With the mock builder each Entry::new() starts empty, so
            // has_mnemonic (which constructs a new Entry internally) always
            // returns false in a fresh test environment.
            assert!(!has_mnemonic());
        }

        #[test]
        fn test_store_mnemonic_succeeds_without_error() {
            setup_mock();
            // Verify that store_mnemonic completes without returning an error.
            // Round-trip verification requires a real credential store; see the
            // ignored test below.
            assert!(store_mnemonic("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").is_ok());
            // Clean up to avoid leaking state if the test runner ever reuses
            // a real credential store backend.
            let _ = delete_mnemonic();
        }

        #[test]
        fn test_delete_mnemonic_is_idempotent() {
            setup_mock();
            // Deleting when no credential exists must not return an error.
            assert!(delete_mnemonic().is_ok());
            // Calling a second time is also safe.
            assert!(delete_mnemonic().is_ok());
        }

        #[test]
        fn test_store_mnemonic_for_id_succeeds_without_error() {
            setup_mock();
            assert!(store_mnemonic_for_id("test-id-1", "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", false).is_ok());
            let _ = delete_mnemonic_for_id("test-id-1");
        }

        #[test]
        fn test_delete_mnemonic_for_id_is_idempotent() {
            setup_mock();
            assert!(delete_mnemonic_for_id("nonexistent-id").is_ok());
            assert!(delete_mnemonic_for_id("nonexistent-id").is_ok());
        }

        #[test]
        fn test_store_keyring_index_succeeds_without_error() {
            setup_mock();
            let json = r#"{"version":1,"accounts":[]}"#;
            assert!(store_keyring_index(json, false).is_ok());
        }

        #[test]
        fn test_load_keyring_index_returns_none_when_not_stored() {
            setup_mock();
            // With the mock builder a fresh Entry has no data, so load returns
            // None.
            assert!(load_keyring_index().is_none());
        }

        // -----------------------------------------------------------------
        // Round-trip tests — require a real system credential store.
        // Run with: cargo test -p host-wallet -- --ignored
        // -----------------------------------------------------------------

        #[test]
        #[ignore = "requires a real system credential daemon (gnome-keyring / kwallet / Windows Credential Manager)"]
        fn test_store_and_load_mnemonic_roundtrip() {
            let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
            store_mnemonic(phrase).expect("store_mnemonic failed");
            let loaded = load_mnemonic().expect("load_mnemonic failed");
            assert_eq!(loaded, phrase);
            delete_mnemonic().expect("delete_mnemonic failed");
        }

        #[test]
        #[ignore = "requires a real system credential daemon (gnome-keyring / kwallet / Windows Credential Manager)"]
        fn test_has_mnemonic_returns_true_after_store() {
            let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
            store_mnemonic(phrase).expect("store_mnemonic failed");
            assert!(has_mnemonic());
            delete_mnemonic().expect("delete_mnemonic failed");
        }

        #[test]
        #[ignore = "requires a real system credential daemon (gnome-keyring / kwallet / Windows Credential Manager)"]
        fn test_delete_mnemonic_removes_entry() {
            let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
            store_mnemonic(phrase).expect("store_mnemonic failed");
            delete_mnemonic().expect("delete_mnemonic failed");
            assert!(!has_mnemonic());
        }

        #[test]
        #[ignore = "requires a real system credential daemon (gnome-keyring / kwallet / Windows Credential Manager)"]
        fn test_store_and_load_mnemonic_for_id_roundtrip() {
            let id = "integration-test-account";
            let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
            store_mnemonic_for_id(id, phrase, false).expect("store_mnemonic_for_id failed");
            let loaded = load_mnemonic_for_id(id)
                .expect("load_mnemonic_for_id failed")
                .expect("expected Some, got None");
            assert_eq!(loaded.as_str(), phrase);
            delete_mnemonic_for_id(id).expect("delete_mnemonic_for_id failed");
        }

        #[test]
        #[ignore = "requires a real system credential daemon (gnome-keyring / kwallet / Windows Credential Manager)"]
        fn test_store_and_load_keyring_index_roundtrip() {
            let json = r#"{"version":1,"accounts":[{"id":"acc1","address":"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"}]}"#;
            store_keyring_index(json, false).expect("store_keyring_index failed");
            let loaded = load_keyring_index().expect("load_keyring_index returned None");
            assert_eq!(loaded, json);
            // Clean up: delete via a direct Entry call since there's no
            // delete_keyring_index function in the public API.
            let entry = keyring::Entry::new(SERVICE, "keyring-index").expect("Entry::new failed");
            let _ = entry.delete_credential();
        }

        // NOTE: These paired_data tests pass trivially under keyring::mock because
        // EntryOnly persistence means each Entry::new() starts empty. A meaningful
        // round-trip test (store then load, assert all four fields) requires a real
        // credential daemon — tracked in #193.
        #[test]
        fn test_has_paired_wallet_returns_false_initially() {
            setup_mock();
            assert!(!has_paired_wallet());
        }

        #[test]
        fn test_load_paired_data_returns_none_initially() {
            setup_mock();
            assert!(load_paired_data().is_none());
        }
    }
}

#[cfg(all(
    not(target_os = "macos"),
    not(target_os = "ios"),
    not(target_os = "android"),
    not(target_arch = "wasm32")
))]
pub use desktop::*;

// ---------------------------------------------------------------------------
// Android — keystore operations are handled by the Kotlin wrapper
// ---------------------------------------------------------------------------
//
// Android Keystore + BiometricPrompt are Java APIs that cannot be called
// from Rust. The Kotlin wrapper (host-sdk-android) provides `AndroidKeystore`
// which stores/loads the mnemonic via Android Keystore with biometric
// authentication, then passes it to `MobileWallet.loadFromMnemonic()`.
//
// These stubs exist so the code compiles for `target_os = "android"`.
//
// IMPORTANT: On Android, `WalletManager::create()` calls `store_mnemonic()`
// which returns an error here. Android apps MUST use `load_from_mnemonic()`
// after storing the mnemonic via Kotlin's `AndroidKeystore`. Calling
// `WalletManager::create()` directly on Android will fail.

#[cfg(target_os = "android")]
pub fn has_mnemonic() -> bool {
    false
}

#[cfg(target_os = "android")]
#[deprecated(
    note = "On Android, use AndroidKeystore from host-sdk-android. This stub always returns Err."
)]
pub fn store_mnemonic(_mnemonic: &str) -> anyhow::Result<()> {
    Err(anyhow::anyhow!(
        "Use AndroidKeystore from host-sdk-android to store the mnemonic"
    ))
}

#[cfg(target_os = "android")]
#[deprecated(
    note = "On Android, use AndroidKeystore from host-sdk-android. This stub always returns Err."
)]
pub fn load_mnemonic() -> anyhow::Result<String> {
    Err(anyhow::anyhow!(
        "Use AndroidKeystore from host-sdk-android to load the mnemonic"
    ))
}

#[cfg(target_os = "android")]
#[deprecated(
    note = "On Android, use AndroidKeystore from host-sdk-android. This stub always returns Err."
)]
pub fn delete_mnemonic() -> anyhow::Result<()> {
    Err(anyhow::anyhow!(
        "Use AndroidKeystore from host-sdk-android to delete the mnemonic"
    ))
}

// Android stubs for KeyRing functions — Android Keystore is managed by Kotlin.

#[cfg(target_os = "android")]
#[deprecated(
    note = "On Android, use AndroidKeystore from host-sdk-android. This stub always returns Err."
)]
pub fn store_mnemonic_for_id(_id: &str, _phrase: &str, _sync: bool) -> anyhow::Result<()> {
    Err(anyhow::anyhow!(
        "Use AndroidKeystore from host-sdk-android to store the mnemonic"
    ))
}

#[cfg(target_os = "android")]
#[deprecated(
    note = "On Android, use AndroidKeystore from host-sdk-android. This stub always returns Err."
)]
pub fn load_mnemonic_for_id(_id: &str) -> anyhow::Result<Option<zeroize::Zeroizing<String>>> {
    Err(anyhow::anyhow!(
        "Use AndroidKeystore from host-sdk-android to load the mnemonic"
    ))
}

#[cfg(target_os = "android")]
#[deprecated(
    note = "On Android, use AndroidKeystore from host-sdk-android. This stub always returns Err."
)]
pub fn delete_mnemonic_for_id(_id: &str) -> anyhow::Result<()> {
    Err(anyhow::anyhow!(
        "Use AndroidKeystore from host-sdk-android to delete the mnemonic"
    ))
}

#[cfg(target_os = "android")]
#[deprecated(
    note = "On Android, use AndroidKeystore from host-sdk-android. This stub always returns Err."
)]
pub fn store_keyring_index(_json: &str, _sync: bool) -> anyhow::Result<()> {
    Err(anyhow::anyhow!(
        "KeyRing index storage is not supported on Android via Rust — use host-sdk-android"
    ))
}

#[cfg(target_os = "android")]
pub fn load_keyring_index() -> Option<String> {
    None
}

#[cfg(target_os = "android")]
#[deprecated(
    note = "On Android, use AndroidKeystore from host-sdk-android. This stub always returns Err."
)]
pub fn store_paired_data(
    _address: &str,
    _session_key: &[u8; 16],
    _identity_pubkey: &[u8; 32],
    _p256_pubkey: &[u8; 65],
) -> anyhow::Result<()> {
    Err(anyhow::anyhow!(
        "Use AndroidKeystore from host-sdk-android to store paired data"
    ))
}

#[cfg(target_os = "android")]
pub fn load_paired_data() -> Option<PairedSessionData> {
    None
}

#[cfg(target_os = "android")]
pub fn has_paired_wallet() -> bool {
    false
}

#[cfg(target_os = "android")]
#[deprecated(
    note = "On Android, use AndroidKeystore from host-sdk-android. This stub always returns Err."
)]
pub fn delete_paired_data() -> anyhow::Result<()> {
    Err(anyhow::anyhow!(
        "Use AndroidKeystore from host-sdk-android to delete paired data"
    ))
}

// ---------------------------------------------------------------------------
// WASM — no keystore available (use load_from_mnemonic instead)
// ---------------------------------------------------------------------------

#[cfg(target_arch = "wasm32")]
pub fn has_mnemonic() -> bool {
    false
}

#[cfg(target_arch = "wasm32")]
pub fn store_mnemonic(_mnemonic: &str) -> anyhow::Result<()> {
    Err(anyhow::anyhow!(
        "Platform keystore unavailable on WASM — use load_from_mnemonic()"
    ))
}

#[cfg(target_arch = "wasm32")]
pub fn load_mnemonic() -> anyhow::Result<String> {
    Err(anyhow::anyhow!(
        "Platform keystore unavailable on WASM — use load_from_mnemonic()"
    ))
}

#[cfg(target_arch = "wasm32")]
pub fn delete_mnemonic() -> anyhow::Result<()> {
    Err(anyhow::anyhow!(
        "Platform keystore unavailable on WASM — use load_from_mnemonic()"
    ))
}

// WASM stubs for KeyRing functions — no keystore available on WASM.

#[cfg(target_arch = "wasm32")]
pub fn store_mnemonic_for_id(_id: &str, _phrase: &str, _sync: bool) -> anyhow::Result<()> {
    Err(anyhow::anyhow!(
        "Platform keystore unavailable on WASM — use load_from_mnemonic()"
    ))
}

#[cfg(target_arch = "wasm32")]
pub fn load_mnemonic_for_id(_id: &str) -> anyhow::Result<Option<zeroize::Zeroizing<String>>> {
    Err(anyhow::anyhow!(
        "Platform keystore unavailable on WASM — use load_from_mnemonic()"
    ))
}

#[cfg(target_arch = "wasm32")]
pub fn delete_mnemonic_for_id(_id: &str) -> anyhow::Result<()> {
    Err(anyhow::anyhow!(
        "Platform keystore unavailable on WASM — use load_from_mnemonic()"
    ))
}

#[cfg(target_arch = "wasm32")]
pub fn store_keyring_index(_json: &str, _sync: bool) -> anyhow::Result<()> {
    Err(anyhow::anyhow!(
        "Platform keystore unavailable on WASM — use load_from_mnemonic()"
    ))
}

#[cfg(target_arch = "wasm32")]
pub fn load_keyring_index() -> Option<String> {
    None
}

#[cfg(target_arch = "wasm32")]
pub fn store_paired_data(
    _address: &str,
    _session_key: &[u8; 16],
    _identity_pubkey: &[u8; 32],
    _p256_pubkey: &[u8; 65],
) -> anyhow::Result<()> {
    Err(anyhow::anyhow!(
        "Platform keystore unavailable on WASM — use load_from_mnemonic()"
    ))
}

#[cfg(target_arch = "wasm32")]
pub fn load_paired_data() -> Option<PairedSessionData> {
    None
}

#[cfg(target_arch = "wasm32")]
pub fn has_paired_wallet() -> bool {
    false
}

#[cfg(target_arch = "wasm32")]
pub fn delete_paired_data() -> anyhow::Result<()> {
    Err(anyhow::anyhow!(
        "Platform keystore unavailable on WASM — use load_from_mnemonic()"
    ))
}

// ---------------------------------------------------------------------------
// macOS keystore integration tests
//
// These tests exercise the real Security Framework Keychain and run as part
// of the `swift-test` CI job on `macos-14`.  They use the legacy keychain
// fallback path (no biometric ACL) because the CI runner does not have an
// enrolled biometric identity.  `store_mnemonic` falls back to the legacy
// keychain automatically when the Data Protection Keychain is unavailable
// (e.g. ad-hoc-signed binary without the Keychain Sharing entitlement).
//
// All tests acquire `KEYCHAIN_MUTEX` before touching the Keychain so that
// Rust's default parallel test runner does not interleave keychain writes
// across tests that share the same service / account namespace.
// ---------------------------------------------------------------------------

#[cfg(all(target_os = "macos", test))]
mod macos_tests {
    use super::macos;
    use std::sync::{Mutex, MutexGuard};

    const TEST_PHRASE: &str =
        "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";

    /// Global lock serialising all macOS keychain tests.
    ///
    /// Rust's test harness runs tests in parallel by default.  Because all
    /// `macos::*` functions share a single `io.parity.host-wallet` service
    /// name in the keychain, concurrent tests would race on the same
    /// credential slots.  Holding this lock for the duration of each test
    /// ensures operations are serialised within the test binary.
    static KEYCHAIN_MUTEX: Mutex<()> = Mutex::new(());

    fn keychain_lock() -> MutexGuard<'static, ()> {
        KEYCHAIN_MUTEX.lock().unwrap_or_else(|e| e.into_inner())
    }

    /// Remove any pre-existing master-mnemonic keychain entry to guarantee a
    /// clean slate for the calling test.
    fn cleanup() {
        let _ = macos::delete_mnemonic();
    }

    #[test]
    fn test_has_mnemonic_returns_false_initially() {
        let _guard = keychain_lock();
        cleanup();
        assert!(!macos::has_mnemonic());
    }

    #[test]
    fn test_store_and_load_mnemonic_roundtrip() {
        let _guard = keychain_lock();
        cleanup();
        macos::store_mnemonic(TEST_PHRASE).expect("store_mnemonic failed");
        let loaded = macos::load_mnemonic().expect("load_mnemonic failed");
        assert_eq!(loaded, TEST_PHRASE);
        cleanup();
    }

    #[test]
    fn test_has_mnemonic_returns_true_after_store() {
        let _guard = keychain_lock();
        cleanup();
        macos::store_mnemonic(TEST_PHRASE).expect("store_mnemonic failed");
        assert!(macos::has_mnemonic());
        cleanup();
    }

    #[test]
    fn test_delete_mnemonic_removes_entry() {
        let _guard = keychain_lock();
        cleanup();
        macos::store_mnemonic(TEST_PHRASE).expect("store_mnemonic failed");
        macos::delete_mnemonic().expect("delete_mnemonic failed");
        assert!(!macos::has_mnemonic());
    }

    #[test]
    fn test_delete_mnemonic_is_idempotent() {
        let _guard = keychain_lock();
        cleanup();
        // Deleting when no entry exists must not return an error.
        assert!(macos::delete_mnemonic().is_ok());
        assert!(macos::delete_mnemonic().is_ok());
    }

    #[test]
    fn test_store_and_load_mnemonic_for_id_roundtrip() {
        let _guard = keychain_lock();
        let id = "test-keystore-id";
        // Clean up before and after.
        let _ = macos::delete_mnemonic_for_id(id);
        macos::store_mnemonic_for_id(id, TEST_PHRASE, false).expect("store_mnemonic_for_id failed");
        let loaded = macos::load_mnemonic_for_id(id)
            .expect("load_mnemonic_for_id failed")
            .expect("expected Some, got None");
        assert_eq!(loaded.as_str(), TEST_PHRASE);
        macos::delete_mnemonic_for_id(id).expect("delete_mnemonic_for_id failed");
    }

    #[test]
    fn test_store_and_load_keyring_index_roundtrip() {
        let _guard = keychain_lock();
        let json = r#"{"version":1,"accounts":[]}"#;
        // Clean up any pre-existing index entry.
        use security_framework::passwords::delete_generic_password;
        let _ = delete_generic_password("io.parity.host-wallet", "keyring-index");

        macos::store_keyring_index(json, false).expect("store_keyring_index failed");
        let loaded = macos::load_keyring_index().expect("load_keyring_index returned None");
        assert_eq!(loaded, json);

        // Clean up.
        let _ = delete_generic_password("io.parity.host-wallet", "keyring-index");
    }

    #[test]
    fn test_has_paired_wallet_returns_false_when_empty() {
        let _lock = keychain_lock();
        // Clean up any leftover paired data
        let _ = macos::delete_paired_data();
        assert!(!macos::has_paired_wallet());
    }

    #[test]
    fn test_load_paired_data_returns_none_when_empty() {
        let _lock = keychain_lock();
        let _ = macos::delete_paired_data();
        assert!(macos::load_paired_data().is_none());
    }

    /// Round-trip test for paired session data store and load.
    ///
    /// On CI the binary is ad-hoc signed (no Keychain Sharing entitlement), so
    /// `store_paired_data` falls back to the legacy keychain automatically.
    /// `load_paired_data` then reads from the legacy keychain. Both paths are
    /// exercised here — the biometric path is tested on real devices.
    #[test]
    fn test_store_and_load_paired_data_roundtrip() {
        let _lock = keychain_lock();
        let _ = macos::delete_paired_data();

        let address = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY";
        let session_key: [u8; 16] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
        let identity_pubkey: [u8; 32] = [0xab; 32];
        let p256_pubkey: [u8; 65] = [0x04; 65];

        macos::store_paired_data(address, &session_key, &identity_pubkey, &p256_pubkey)
            .expect("store_paired_data failed");

        let (loaded_addr, loaded_sk, loaded_id_pk, loaded_p256) =
            macos::load_paired_data().expect("load_paired_data returned None");

        assert_eq!(loaded_addr, address);
        assert_eq!(loaded_sk, session_key);
        assert_eq!(loaded_id_pk, identity_pubkey);
        assert_eq!(loaded_p256, p256_pubkey);

        let _ = macos::delete_paired_data();
    }

    /// Verify that `delete_paired_data` clears both keychain tiers so that
    /// `has_paired_wallet` returns false after deletion.
    #[test]
    fn test_delete_paired_data_clears_both_keychains() {
        let _lock = keychain_lock();
        let _ = macos::delete_paired_data();

        let address = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY";
        let session_key: [u8; 16] = [0xde; 16];
        let identity_pubkey: [u8; 32] = [0xbe; 32];
        let p256_pubkey: [u8; 65] = [0x04; 65];

        macos::store_paired_data(address, &session_key, &identity_pubkey, &p256_pubkey)
            .expect("store_paired_data failed");
        assert!(
            macos::has_paired_wallet(),
            "expected has_paired_wallet to be true after store"
        );

        macos::delete_paired_data().expect("delete_paired_data failed");
        assert!(
            !macos::has_paired_wallet(),
            "expected has_paired_wallet to be false after delete"
        );
    }
}
