//! UniFFI bindings for nostr-double-ratchet
//!
//! This crate provides FFI-friendly wrappers around the core nostr-double-ratchet
//! library for use in iOS and Android applications via UniFFI.

use std::sync::{Arc, Mutex};
use std::time::{SystemTime, UNIX_EPOCH};

use nostr_double_ratchet::{
    AppKeys, CreateGroupOptions, DeviceEntry, FileStorageAdapter, GroupData, GroupDecryptedEvent,
    GroupSendEvent, InMemoryStorage, Invite, NdrRuntime, Session, SessionManagerEvent,
    SessionState, StorageAdapter,
};

mod error;
pub use error::NdrError;

/// Returns the version of the ndr-ffi crate.
#[uniffi::export]
pub fn version() -> String {
    env!("CARGO_PKG_VERSION").to_string()
}

/// FFI-friendly keypair with hex-encoded keys.
#[derive(uniffi::Record)]
pub struct FfiKeyPair {
    pub public_key_hex: String,
    pub private_key_hex: String,
}

/// Result of accepting an invite.
#[derive(uniffi::Record)]
pub struct InviteAcceptResult {
    pub session: Arc<SessionHandle>,
    pub response_event_json: String,
}

/// Result of sending a message.
#[derive(uniffi::Record)]
pub struct SendResult {
    pub outer_event_json: String,
    pub inner_event_json: String,
}

/// Result of decrypting a message.
#[derive(uniffi::Record)]
pub struct DecryptResult {
    pub plaintext: String,
    pub inner_event_json: String,
}

/// Event emitted by SessionManager for external publish/subscribe handling.
#[derive(uniffi::Record)]
pub struct PubSubEvent {
    pub kind: String,
    pub subid: Option<String>,
    pub filter_json: Option<String>,
    pub event_json: Option<String>,
    pub sender_pubkey_hex: Option<String>,
    pub content: Option<String>,
    pub event_id: Option<String>,
}

/// Result of accepting an invite through SessionManager.
#[derive(uniffi::Record)]
pub struct SessionManagerAcceptInviteResult {
    pub owner_pubkey_hex: String,
    pub inviter_device_pubkey_hex: String,
    pub device_id: String,
    pub created_new_session: bool,
}

/// FFI-friendly group metadata payload.
#[derive(uniffi::Record)]
pub struct FfiGroupData {
    pub id: String,
    pub name: String,
    pub description: Option<String>,
    pub picture: Option<String>,
    pub members: Vec<String>,
    pub admins: Vec<String>,
    pub created_at_ms: u64,
    pub secret: Option<String>,
    pub accepted: Option<bool>,
}

/// Result of sending a group event through GroupManager.
#[derive(uniffi::Record)]
pub struct GroupSendResult {
    pub outer_event_json: String,
    pub inner_event_json: String,
    pub outer_event_id: String,
    pub inner_event_id: String,
}

/// Metadata fanout summary for group creation.
#[derive(uniffi::Record)]
pub struct GroupCreateFanout {
    pub enabled: bool,
    pub attempted: u64,
    pub succeeded: Vec<String>,
    pub failed: Vec<String>,
}

/// Result of creating a group through GroupManager.
#[derive(uniffi::Record)]
pub struct GroupCreateResult {
    pub group: FfiGroupData,
    pub metadata_rumor_json: Option<String>,
    pub fanout: GroupCreateFanout,
}

/// Decrypted group event returned by GroupManager.
#[derive(uniffi::Record)]
pub struct GroupDecryptedResult {
    pub group_id: String,
    pub sender_event_pubkey_hex: String,
    pub sender_device_pubkey_hex: String,
    pub sender_owner_pubkey_hex: Option<String>,
    pub outer_event_id: String,
    pub outer_created_at: u64,
    pub key_id: u32,
    pub message_number: u32,
    pub inner_event_json: String,
    pub inner_event_id: String,
}

/// Shared outer-subscription sync plan for group sender-event authors.
#[derive(uniffi::Record)]
pub struct GroupOuterSubscriptionPlanResult {
    pub authors: Vec<String>,
    pub added_authors: Vec<String>,
}

/// Session-state snapshot used to inspect message-push routing without reading storage files.
#[derive(uniffi::Record)]
pub struct MessagePushSessionStateResult {
    pub state_json: String,
    pub tracked_sender_pubkeys: Vec<String>,
    pub has_receiving_capability: bool,
}

/// Generate a new keypair.
#[uniffi::export]
pub fn generate_keypair() -> FfiKeyPair {
    let keys = nostr::Keys::generate();
    FfiKeyPair {
        public_key_hex: keys.public_key().to_hex(),
        private_key_hex: keys.secret_key().to_secret_hex(),
    }
}

/// Derive a public key from a hex-encoded private key.
#[uniffi::export]
pub fn derive_public_key(privkey_hex: String) -> Result<String, NdrError> {
    let privkey = parse_private_key(&privkey_hex)?;
    let secret_key =
        nostr::SecretKey::from_slice(&privkey).map_err(|e| NdrError::InvalidKey(e.to_string()))?;
    Ok(nostr::Keys::new(secret_key).public_key().to_hex())
}

/// FFI-friendly device entry for AppKeys.
#[derive(uniffi::Record)]
pub struct FfiDeviceEntry {
    pub identity_pubkey_hex: String,
    pub created_at: u64,
    pub device_label: Option<String>,
    pub client_label: Option<String>,
}

fn ffi_device_entry_from_app_keys(app_keys: &AppKeys, device: DeviceEntry) -> FfiDeviceEntry {
    let labels = app_keys.get_device_labels(&device.identity_pubkey);
    FfiDeviceEntry {
        identity_pubkey_hex: hex::encode(device.identity_pubkey.to_bytes()),
        created_at: device.created_at,
        device_label: labels.and_then(|label| label.device_label.clone()),
        client_label: labels.and_then(|label| label.client_label.clone()),
    }
}

fn owner_keys_from_privkey_hex(owner_privkey_hex: &str) -> Result<nostr::Keys, NdrError> {
    let owner_privkey = parse_private_key(owner_privkey_hex)?;
    let owner_sk = nostr::SecretKey::from_slice(&owner_privkey)
        .map_err(|e| NdrError::InvalidKey(e.to_string()))?;
    Ok(nostr::Keys::new(owner_sk))
}

fn parse_app_keys_for_owner(
    event: &nostr::Event,
    owner_privkey_hex: Option<&str>,
) -> Result<AppKeys, NdrError> {
    match owner_privkey_hex {
        Some(privkey_hex) => {
            let owner_keys = owner_keys_from_privkey_hex(privkey_hex)?;
            Ok(AppKeys::from_event_with_labels(event, &owner_keys)?)
        }
        None => Ok(AppKeys::from_event(event)?),
    }
}

/// Create a signed AppKeys event JSON for publishing to relays.
#[uniffi::export]
pub fn create_signed_app_keys_event(
    owner_pubkey_hex: String,
    owner_privkey_hex: String,
    devices: Vec<FfiDeviceEntry>,
) -> Result<String, NdrError> {
    let owner_pubkey = nostr_double_ratchet::utils::pubkey_from_hex(&owner_pubkey_hex)?;
    let owner_keys = owner_keys_from_privkey_hex(&owner_privkey_hex)?;
    if owner_keys.public_key() != owner_pubkey {
        return Err(NdrError::InvalidKey(
            "owner pubkey does not match owner private key".to_string(),
        ));
    }

    let entries = devices
        .iter()
        .filter_map(|d| {
            let pk = nostr_double_ratchet::utils::pubkey_from_hex(&d.identity_pubkey_hex).ok()?;
            Some(DeviceEntry::new(pk, d.created_at))
        })
        .collect::<Vec<_>>();

    let mut app_keys = AppKeys::new(entries);
    for device in devices {
        let Ok(identity_pubkey) =
            nostr_double_ratchet::utils::pubkey_from_hex(&device.identity_pubkey_hex)
        else {
            continue;
        };

        if device.device_label.is_none() && device.client_label.is_none() {
            continue;
        }

        app_keys.set_device_labels(
            identity_pubkey,
            device.device_label,
            device.client_label,
            None,
        );
    }
    let unsigned = app_keys.get_encrypted_event(&owner_keys)?;
    let signed = unsigned
        .sign_with_keys(&owner_keys)
        .map_err(|e| NdrError::Serialization(e.to_string()))?;
    Ok(serde_json::to_string(&signed)?)
}

/// Parse an AppKeys event JSON and return the contained device entries.
#[uniffi::export]
pub fn parse_app_keys_event(
    event_json: String,
    owner_privkey_hex: Option<String>,
) -> Result<Vec<FfiDeviceEntry>, NdrError> {
    let event: nostr::Event = serde_json::from_str(&event_json)?;
    let app_keys = parse_app_keys_for_owner(&event, owner_privkey_hex.as_deref())?;
    Ok(app_keys
        .get_all_devices()
        .into_iter()
        .map(|d| ffi_device_entry_from_app_keys(&app_keys, d))
        .collect())
}

/// Resolve the latest authorized device list from a set of AppKeys event JSON strings.
#[uniffi::export]
pub fn resolve_latest_app_keys_devices(
    event_jsons: Vec<String>,
    owner_privkey_hex: Option<String>,
) -> Result<Vec<FfiDeviceEntry>, NdrError> {
    let events = event_jsons
        .iter()
        .filter_map(|event_json| serde_json::from_str::<nostr::Event>(event_json).ok())
        .collect::<Vec<_>>();

    let mut latest: Option<nostr_double_ratchet::AppKeysSnapshot> = None;

    for event in events.iter() {
        if !nostr_double_ratchet::is_app_keys_event(event) {
            continue;
        }

        let Ok(app_keys) = parse_app_keys_for_owner(event, owner_privkey_hex.as_deref()) else {
            continue;
        };

        latest = Some(match latest.as_ref() {
            Some(current) => nostr_double_ratchet::apply_app_keys_snapshot(
                Some(&current.app_keys),
                current.created_at,
                &app_keys,
                event.created_at.as_secs(),
            ),
            None => nostr_double_ratchet::AppKeysSnapshot {
                decision: nostr_double_ratchet::AppKeysSnapshotDecision::Advanced,
                app_keys,
                created_at: event.created_at.as_secs(),
            },
        });
    }

    let Some(snapshot) = latest else {
        return Ok(Vec::new());
    };

    Ok(snapshot
        .app_keys
        .get_all_devices()
        .into_iter()
        .map(|d| ffi_device_entry_from_app_keys(&snapshot.app_keys, d))
        .collect())
}

/// Resolve conversation routing candidates for a decrypted rumor.
#[uniffi::export]
pub fn resolve_conversation_candidate_pubkeys(
    owner_pubkey_hex: String,
    rumor_pubkey_hex: String,
    rumor_tags: Vec<Vec<String>>,
    sender_pubkey_hex: String,
) -> Vec<String> {
    nostr_double_ratchet::resolve_conversation_candidate_pubkeys(
        &owner_pubkey_hex,
        &rumor_pubkey_hex,
        &rumor_tags,
        &sender_pubkey_hex,
    )
}

/// FFI wrapper for Invite.
#[derive(uniffi::Object)]
pub struct InviteHandle {
    inner: Mutex<Invite>,
}

#[uniffi::export]
impl InviteHandle {
    /// Create a new invite.
    #[uniffi::constructor]
    pub fn create_new(
        inviter_pubkey_hex: String,
        device_id: Option<String>,
        max_uses: Option<u32>,
    ) -> Result<Arc<Self>, NdrError> {
        let inviter = nostr_double_ratchet::utils::pubkey_from_hex(&inviter_pubkey_hex)?;
        let invite = Invite::create_new(inviter, device_id, max_uses.map(|n| n as usize))?;
        Ok(Arc::new(Self {
            inner: Mutex::new(invite),
        }))
    }

    /// Parse an invite from a URL.
    #[uniffi::constructor]
    pub fn from_url(url: String) -> Result<Arc<Self>, NdrError> {
        let invite = Invite::from_url(&url)?;
        Ok(Arc::new(Self {
            inner: Mutex::new(invite),
        }))
    }

    /// Parse an invite from a Nostr event JSON.
    #[uniffi::constructor]
    pub fn from_event_json(event_json: String) -> Result<Arc<Self>, NdrError> {
        let event: nostr::Event = serde_json::from_str(&event_json)?;
        let invite = Invite::from_event(&event)?;
        Ok(Arc::new(Self {
            inner: Mutex::new(invite),
        }))
    }

    /// Deserialize an invite from JSON.
    #[uniffi::constructor]
    pub fn deserialize(json: String) -> Result<Arc<Self>, NdrError> {
        let invite = Invite::deserialize(&json)?;
        Ok(Arc::new(Self {
            inner: Mutex::new(invite),
        }))
    }

    /// Convert the invite to a shareable URL.
    pub fn to_url(&self, root: String) -> Result<String, NdrError> {
        let invite = self.inner.lock().unwrap();
        Ok(invite.get_url(&root)?)
    }

    /// Convert the invite to a Nostr event JSON.
    pub fn to_event_json(&self) -> Result<String, NdrError> {
        let invite = self.inner.lock().unwrap();
        let event = invite.get_event()?;
        Ok(serde_json::to_string(&event)?)
    }

    /// Serialize the invite to JSON for persistence.
    pub fn serialize(&self) -> Result<String, NdrError> {
        let invite = self.inner.lock().unwrap();
        Ok(invite.serialize()?)
    }

    /// Accept the invite and create a session.
    pub fn accept(
        &self,
        invitee_pubkey_hex: String,
        invitee_privkey_hex: String,
        device_id: Option<String>,
    ) -> Result<InviteAcceptResult, NdrError> {
        let invite = self.inner.lock().unwrap();
        let invitee_pubkey = nostr_double_ratchet::utils::pubkey_from_hex(&invitee_pubkey_hex)?;
        let invitee_privkey = parse_private_key(&invitee_privkey_hex)?;

        let (session, response_event) =
            invite.accept(invitee_pubkey, invitee_privkey, device_id)?;
        let response_event_json = serde_json::to_string(&response_event)?;

        Ok(InviteAcceptResult {
            session: Arc::new(SessionHandle {
                inner: Mutex::new(session),
            }),
            response_event_json,
        })
    }

    /// Accept the invite as an owner and include the owner pubkey in the response payload.
    pub fn accept_with_owner(
        &self,
        invitee_pubkey_hex: String,
        invitee_privkey_hex: String,
        device_id: Option<String>,
        owner_pubkey_hex: Option<String>,
    ) -> Result<InviteAcceptResult, NdrError> {
        let invite = self.inner.lock().unwrap();
        let invitee_pubkey = nostr_double_ratchet::utils::pubkey_from_hex(&invitee_pubkey_hex)?;
        let invitee_privkey = parse_private_key(&invitee_privkey_hex)?;
        let owner_pubkey = match owner_pubkey_hex {
            Some(h) => Some(nostr_double_ratchet::utils::pubkey_from_hex(&h)?),
            None => None,
        };

        let (session, response_event) =
            invite.accept_with_owner(invitee_pubkey, invitee_privkey, device_id, owner_pubkey)?;
        let response_event_json = serde_json::to_string(&response_event)?;

        Ok(InviteAcceptResult {
            session: Arc::new(SessionHandle {
                inner: Mutex::new(session),
            }),
            response_event_json,
        })
    }

    /// Update the invite purpose (e.g. \"link\").
    pub fn set_purpose(&self, purpose: Option<String>) {
        let mut invite = self.inner.lock().unwrap();
        invite.purpose = purpose;
    }

    /// Update the owner pubkey embedded in invite URLs.
    pub fn set_owner_pubkey_hex(&self, owner_pubkey_hex: Option<String>) -> Result<(), NdrError> {
        let mut invite = self.inner.lock().unwrap();
        invite.owner_public_key = match owner_pubkey_hex {
            Some(h) => Some(nostr_double_ratchet::utils::pubkey_from_hex(&h)?),
            None => None,
        };
        Ok(())
    }

    /// Process an invite response event and create a session (inviter side).
    ///
    /// Returns `None` if the event is not a valid response for this invite.
    pub fn process_response(
        &self,
        event_json: String,
        inviter_privkey_hex: String,
    ) -> Result<Option<InviteProcessResult>, NdrError> {
        let invite = self.inner.lock().unwrap();
        let event: nostr::Event = serde_json::from_str(&event_json)?;
        let inviter_privkey = parse_private_key(&inviter_privkey_hex)?;

        let response = invite.process_invite_response(&event, inviter_privkey)?;
        let Some(response) = response else {
            return Ok(None);
        };

        Ok(Some(InviteProcessResult {
            session: Arc::new(SessionHandle {
                inner: Mutex::new(response.session),
            }),
            invitee_pubkey_hex: response.invitee_identity.to_hex(),
            device_id: response.device_id,
            owner_pubkey_hex: response.owner_public_key.map(|pk| pk.to_hex()),
        }))
    }

    /// Get the inviter's public key as hex.
    pub fn get_inviter_pubkey_hex(&self) -> String {
        let invite = self.inner.lock().unwrap();
        invite.inviter.to_hex()
    }

    /// Get the shared secret as hex.
    pub fn get_shared_secret_hex(&self) -> String {
        let invite = self.inner.lock().unwrap();
        hex::encode(invite.shared_secret)
    }
}

/// FFI wrapper for Session.
#[derive(uniffi::Object)]
pub struct SessionHandle {
    inner: Mutex<Session>,
}

#[uniffi::export]
impl SessionHandle {
    /// Initialize a new session.
    #[uniffi::constructor]
    pub fn init(
        their_ephemeral_pubkey_hex: String,
        our_ephemeral_privkey_hex: String,
        is_initiator: bool,
        shared_secret_hex: String,
        name: Option<String>,
    ) -> Result<Arc<Self>, NdrError> {
        let their_pubkey =
            nostr_double_ratchet::utils::pubkey_from_hex(&their_ephemeral_pubkey_hex)?;
        let our_privkey = parse_private_key(&our_ephemeral_privkey_hex)?;
        let shared_secret = parse_secret(&shared_secret_hex)?;

        let session = Session::init(their_pubkey, our_privkey, is_initiator, shared_secret, name)?;

        Ok(Arc::new(Self {
            inner: Mutex::new(session),
        }))
    }

    /// Restore a session from serialized state JSON.
    #[uniffi::constructor]
    pub fn from_state_json(state_json: String) -> Result<Arc<Self>, NdrError> {
        let state: SessionState =
            nostr_double_ratchet::utils::deserialize_session_state(&state_json)?;
        let session = Session::new(state, "restored".to_string());

        Ok(Arc::new(Self {
            inner: Mutex::new(session),
        }))
    }

    /// Serialize the session state to JSON.
    pub fn state_json(&self) -> Result<String, NdrError> {
        let session = self.inner.lock().unwrap();
        Ok(nostr_double_ratchet::utils::serialize_session_state(
            &session.state,
        )?)
    }

    /// Check if the session is ready to send messages.
    pub fn can_send(&self) -> bool {
        let session = self.inner.lock().unwrap();
        session.can_send()
    }

    /// Send a text message.
    pub fn send_text(&self, text: String) -> Result<SendResult, NdrError> {
        let mut session = self.inner.lock().unwrap();
        let outer_event = session.send(text.clone())?;

        // Create inner event representation
        let inner_event = nostr::EventBuilder::text_note(text);
        let inner_event_json =
            serde_json::to_string(&inner_event.build(nostr::Keys::generate().public_key()))?;

        Ok(SendResult {
            outer_event_json: serde_json::to_string(&outer_event)?,
            inner_event_json,
        })
    }

    /// Decrypt a received event.
    pub fn decrypt_event(&self, outer_event_json: String) -> Result<DecryptResult, NdrError> {
        let mut session = self.inner.lock().unwrap();
        let event: nostr::Event = serde_json::from_str(&outer_event_json)?;

        let plaintext = session.receive(&event)?.unwrap_or_default();

        // Try to parse inner event if it's JSON
        let inner_event_json = if plaintext.starts_with('{') {
            plaintext.clone()
        } else {
            // Wrap plain text in a simple structure
            serde_json::json!({
                "content": plaintext
            })
            .to_string()
        };

        Ok(DecryptResult {
            plaintext,
            inner_event_json,
        })
    }

    /// Check if an event is a double-ratchet message.
    pub fn is_dr_message(&self, event_json: String) -> bool {
        if let Ok(event) = serde_json::from_str::<nostr::Event>(&event_json) {
            event.kind == nostr::Kind::Custom(nostr_double_ratchet::MESSAGE_EVENT_KIND as u16)
        } else {
            false
        }
    }
}

/// FFI wrapper for SessionManager.
#[derive(uniffi::Object)]
pub struct SessionManagerHandle {
    runtime: NdrRuntime,
}

#[uniffi::export]
impl SessionManagerHandle {
    /// Create a new session manager with an internal event queue.
    #[uniffi::constructor]
    pub fn new(
        our_pubkey_hex: String,
        our_identity_privkey_hex: String,
        device_id: String,
        owner_pubkey_hex: Option<String>,
    ) -> Result<Arc<Self>, NdrError> {
        let our_pubkey = nostr_double_ratchet::utils::pubkey_from_hex(&our_pubkey_hex)?;
        let our_identity_key = parse_private_key(&our_identity_privkey_hex)?;
        let owner_pubkey = match owner_pubkey_hex {
            Some(h) => nostr_double_ratchet::utils::pubkey_from_hex(&h)?,
            None => our_pubkey,
        };

        let storage: Arc<dyn StorageAdapter> = Arc::new(InMemoryStorage::new());
        let runtime = NdrRuntime::new(
            our_pubkey,
            our_identity_key,
            device_id,
            owner_pubkey,
            Some(storage),
            None,
        );

        Ok(Arc::new(Self { runtime }))
    }

    /// Create a new session manager with file-backed storage.
    #[uniffi::constructor]
    pub fn new_with_storage_path(
        our_pubkey_hex: String,
        our_identity_privkey_hex: String,
        device_id: String,
        storage_path: String,
        owner_pubkey_hex: Option<String>,
    ) -> Result<Arc<Self>, NdrError> {
        let our_pubkey = nostr_double_ratchet::utils::pubkey_from_hex(&our_pubkey_hex)?;
        let our_identity_key = parse_private_key(&our_identity_privkey_hex)?;
        let owner_pubkey = match owner_pubkey_hex {
            Some(h) => nostr_double_ratchet::utils::pubkey_from_hex(&h)?,
            None => our_pubkey,
        };

        let storage = FileStorageAdapter::new(std::path::PathBuf::from(storage_path))
            .map_err(NdrError::from)?;
        let storage: Arc<dyn StorageAdapter> = Arc::new(storage);

        let runtime = NdrRuntime::new(
            our_pubkey,
            our_identity_key,
            device_id,
            owner_pubkey,
            Some(storage),
            None,
        );

        Ok(Arc::new(Self { runtime }))
    }

    /// Initialize the session manager (loads state, creates device invite, subscribes).
    pub fn init(&self) -> Result<(), NdrError> {
        self.runtime.init()?;
        Ok(())
    }

    /// Subscribe to a user's AppKeys/device-invite streams and converge sessions.
    pub fn setup_user(&self, user_pubkey_hex: String) -> Result<(), NdrError> {
        let user_pubkey = nostr_double_ratchet::utils::pubkey_from_hex(&user_pubkey_hex)?;
        self.runtime.setup_user(user_pubkey)?;
        Ok(())
    }

    /// Accept an invite URL using SessionManager's owner-aware routing/auth checks.
    ///
    /// This flow also emits the signed invite response via SessionManager pubsub events,
    /// so hosts should continue draining and publishing `publish_signed` events.
    pub fn accept_invite_from_url(
        &self,
        invite_url: String,
        owner_pubkey_hint_hex: Option<String>,
    ) -> Result<SessionManagerAcceptInviteResult, NdrError> {
        let invite = Invite::from_url(&invite_url)?;
        let owner_pubkey_hint = owner_pubkey_hint_hex
            .as_deref()
            .map(nostr_double_ratchet::utils::pubkey_from_hex)
            .transpose()?;

        let accepted = self.runtime.accept_invite(&invite, owner_pubkey_hint)?;

        Ok(SessionManagerAcceptInviteResult {
            owner_pubkey_hex: accepted.owner_pubkey.to_hex(),
            inviter_device_pubkey_hex: accepted.inviter_device_pubkey.to_hex(),
            device_id: accepted.device_id,
            created_new_session: accepted.created_new_session,
        })
    }

    /// Accept an invite event JSON using SessionManager's owner-aware routing/auth checks.
    pub fn accept_invite_from_event_json(
        &self,
        event_json: String,
        owner_pubkey_hint_hex: Option<String>,
    ) -> Result<SessionManagerAcceptInviteResult, NdrError> {
        let event: nostr::Event = serde_json::from_str(&event_json)?;
        let invite = Invite::from_event(&event)?;
        let owner_pubkey_hint = owner_pubkey_hint_hex
            .as_deref()
            .map(nostr_double_ratchet::utils::pubkey_from_hex)
            .transpose()?;

        let accepted = self.runtime.accept_invite(&invite, owner_pubkey_hint)?;

        Ok(SessionManagerAcceptInviteResult {
            owner_pubkey_hex: accepted.owner_pubkey.to_hex(),
            inviter_device_pubkey_hex: accepted.inviter_device_pubkey.to_hex(),
            device_id: accepted.device_id,
            created_new_session: accepted.created_new_session,
        })
    }

    /// Send a text message to a recipient.
    pub fn send_text(
        &self,
        recipient_pubkey_hex: String,
        text: String,
        expires_at_seconds: Option<u64>,
    ) -> Result<Vec<String>, NdrError> {
        let recipient = nostr_double_ratchet::utils::pubkey_from_hex(&recipient_pubkey_hex)?;
        let options = expires_at_seconds.map(|expires_at| nostr_double_ratchet::SendOptions {
            expires_at: Some(expires_at),
            ttl_seconds: None,
        });
        Ok(self.runtime.send_text(recipient, text, options)?)
    }

    /// Send a text message and return both the stable inner (rumor) id and the
    /// list of outer message event ids that were published.
    pub fn send_text_with_inner_id(
        &self,
        recipient_pubkey_hex: String,
        text: String,
        expires_at_seconds: Option<u64>,
    ) -> Result<SendTextResult, NdrError> {
        let recipient = nostr_double_ratchet::utils::pubkey_from_hex(&recipient_pubkey_hex)?;
        let options = expires_at_seconds.map(|expires_at| nostr_double_ratchet::SendOptions {
            expires_at: Some(expires_at),
            ttl_seconds: None,
        });
        let (inner_id, outer_event_ids) = self
            .runtime
            .send_text_with_inner_id(recipient, text, options)?;
        Ok(SendTextResult {
            inner_id,
            outer_event_ids,
        })
    }

    /// Send an arbitrary inner rumor event to a recipient, returning stable inner id + outer ids.
    ///
    /// This is used for group chats where we need custom kinds/tags (e.g. group metadata kind 40,
    /// group-tagged chat messages kind 14, reactions kind 7, typing kind 25).
    ///
    /// The caller controls the inner rumor tags via `tags_json` (JSON array of string arrays).
    /// For group fan-out, do NOT include recipient-specific tags like `["p", <recipient>]` so
    /// the inner rumor id stays stable across all recipients.
    pub fn send_event_with_inner_id(
        &self,
        recipient_pubkey_hex: String,
        kind: u32,
        content: String,
        tags_json: String,
        created_at_seconds: Option<u64>,
    ) -> Result<SendTextResult, NdrError> {
        let recipient = nostr_double_ratchet::utils::pubkey_from_hex(&recipient_pubkey_hex)?;

        // Parse tags from JSON (array of string arrays).
        let tags_vec: Vec<Vec<String>> = if tags_json.trim().is_empty() {
            Vec::new()
        } else {
            serde_json::from_str(&tags_json)?
        };

        // Try to reuse an explicit ms tag as the created_at source when the caller didn't
        // provide created_at_seconds (helps keep ids stable across repeated fan-out calls).
        let mut ms_value: Option<u64> = None;
        for t in tags_vec.iter() {
            if t.first().map(|s| s.as_str()) != Some("ms") {
                continue;
            }
            if let Some(v) = t.get(1) {
                ms_value = v.parse::<u64>().ok();
                break;
            }
        }

        let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
        let now_s = now.as_secs();
        let now_ms = now.as_millis() as u64;

        let created_at_s = created_at_seconds
            .or_else(|| ms_value.map(|ms| ms / 1000))
            .unwrap_or(now_s);

        let mut tags: Vec<nostr::Tag> = Vec::with_capacity(tags_vec.len() + 1);
        let mut has_ms = false;

        for t in tags_vec {
            if t.first().map(|s| s.as_str()) == Some("ms") {
                has_ms = true;
            }
            tags.push(nostr::Tag::parse(&t).map_err(|e| NdrError::InvalidEvent(e.to_string()))?);
        }

        if !has_ms {
            tags.push(
                nostr::Tag::parse(&["ms".to_string(), now_ms.to_string()])
                    .map_err(|e| NdrError::InvalidEvent(e.to_string()))?,
            );
        }

        let kind_u16: u16 = kind
            .try_into()
            .map_err(|_| NdrError::InvalidEvent("kind out of range".into()))?;

        let owner_pubkey = self.runtime.get_owner_pubkey();

        let mut event = nostr::EventBuilder::new(nostr::Kind::from(kind_u16), &content)
            .tags(tags)
            .custom_created_at(nostr::Timestamp::from(created_at_s))
            .build(owner_pubkey);

        event.ensure_id();
        let inner_id = event
            .id
            .as_ref()
            .map(|id| id.to_string())
            .unwrap_or_default();

        let outer_event_ids = self.runtime.send_event(recipient, event)?;

        Ok(SendTextResult {
            inner_id,
            outer_event_ids,
        })
    }

    /// Send an already-built rumor JSON to a recipient without rebuilding it.
    ///
    /// This preserves the original rumor pubkey, timestamp, tags, and id.
    pub fn send_rumor_json(
        &self,
        recipient_pubkey_hex: String,
        rumor_json: String,
    ) -> Result<SendTextResult, NdrError> {
        let recipient = nostr_double_ratchet::utils::pubkey_from_hex(&recipient_pubkey_hex)?;
        let event: nostr::UnsignedEvent = serde_json::from_str(&rumor_json)?;
        let inner_id = unsigned_event_id_string(&event);
        let outer_event_ids = self.runtime.send_event(recipient, event)?;

        Ok(SendTextResult {
            inner_id,
            outer_event_ids,
        })
    }

    /// Upsert group metadata into the embedded GroupManager.
    pub fn group_upsert(&self, group: FfiGroupData) -> Result<(), NdrError> {
        self.runtime.with_group_context(|_, group_manager, _| {
            group_manager.upsert_group(ffi_group_data_to_group_data(group))
        })?;
        Ok(())
    }

    /// Create a group through the embedded GroupManager, with optional metadata fanout.
    pub fn group_create(
        &self,
        name: String,
        member_owner_pubkeys: Vec<String>,
        fanout_metadata: Option<bool>,
        now_ms: Option<u64>,
    ) -> Result<GroupCreateResult, NdrError> {
        let member_refs: Vec<&str> = member_owner_pubkeys.iter().map(String::as_str).collect();
        let should_fanout = fanout_metadata.unwrap_or(true);

        self.runtime
            .with_group_context(|session_manager, group_manager, _| {
                let mut send_pairwise = |recipient_owner: nostr::PublicKey,
                                         rumor: &nostr::UnsignedEvent|
                 -> nostr_double_ratchet::Result<()> {
                    session_manager.send_event(recipient_owner, rumor.clone())?;
                    Ok(())
                };

                let mut opts = CreateGroupOptions {
                    send_pairwise: None,
                    fanout_metadata: should_fanout,
                    now_ms,
                };
                if should_fanout {
                    opts.send_pairwise = Some(&mut send_pairwise);
                }

                let created = group_manager.create_group(&name, &member_refs, opts)?;
                let metadata_rumor_json = created
                    .metadata_rumor
                    .as_ref()
                    .map(serde_json::to_string)
                    .transpose()?;

                Ok(GroupCreateResult {
                    group: group_data_to_ffi_group_data(created.group),
                    metadata_rumor_json,
                    fanout: GroupCreateFanout {
                        enabled: created.fanout.enabled,
                        attempted: created.fanout.attempted as u64,
                        succeeded: created.fanout.succeeded,
                        failed: created.fanout.failed,
                    },
                })
            })
    }

    /// Remove a group from the embedded GroupManager.
    pub fn group_remove(&self, group_id: String) {
        self.runtime
            .with_group_context(|_, group_manager, _| group_manager.remove_group(&group_id));
    }

    /// Return known sender-event pubkeys used for one-to-many group transport.
    pub fn group_known_sender_event_pubkeys(&self) -> Vec<String> {
        self.runtime.with_group_context(|_, group_manager, _| {
            group_manager
                .known_sender_event_pubkeys()
                .into_iter()
                .map(|pk| pk.to_hex())
                .collect()
        })
    }

    /// Return the current group outer authors and which ones were newly added
    /// since the last sync plan request for this handle.
    pub fn group_outer_subscription_plan(&self) -> GroupOuterSubscriptionPlanResult {
        self.runtime.with_group_context(|_, group_manager, _| {
            let plan = group_manager.outer_subscription_plan();
            GroupOuterSubscriptionPlanResult {
                authors: plan.authors.into_iter().map(|pk| pk.to_hex()).collect(),
                added_authors: plan
                    .added_authors
                    .into_iter()
                    .map(|pk| pk.to_hex())
                    .collect(),
            }
        })
    }

    /// Send a group event through GroupManager.
    ///
    /// Pairwise sender-key distribution rumors are sent through SessionManager sessions.
    /// The encrypted one-to-many outer event is emitted via the SessionManager pubsub queue.
    pub fn group_send_event(
        &self,
        group_id: String,
        kind: u32,
        content: String,
        tags_json: String,
        now_ms: Option<u64>,
    ) -> Result<GroupSendResult, NdrError> {
        let tags: Vec<Vec<String>> = if tags_json.trim().is_empty() {
            Vec::new()
        } else {
            serde_json::from_str(&tags_json)?
        };

        self.runtime
            .with_group_context(|session_manager, group_manager, event_tx| {
                let mut send_pairwise = |recipient_owner: nostr::PublicKey,
                                         rumor: &nostr::UnsignedEvent|
                 -> nostr_double_ratchet::Result<()> {
                    session_manager.send_event(recipient_owner, rumor.clone())?;
                    Ok(())
                };

                let mut publish_outer = |outer: &nostr::Event| -> nostr_double_ratchet::Result<()> {
                    event_tx
                        .send(SessionManagerEvent::PublishSigned(outer.clone()))
                        .map_err(|e| nostr_double_ratchet::Error::Storage(e.to_string()))?;
                    Ok(())
                };

                let result = group_manager.send_event(
                    &group_id,
                    GroupSendEvent {
                        kind,
                        content,
                        tags,
                    },
                    &mut send_pairwise,
                    &mut publish_outer,
                    now_ms,
                )?;

                Ok(GroupSendResult {
                    outer_event_json: serde_json::to_string(&result.outer)?,
                    inner_event_json: serde_json::to_string(&result.inner)?,
                    outer_event_id: result.outer.id.to_string(),
                    inner_event_id: unsigned_event_id_string(&result.inner),
                })
            })
    }

    /// Handle a decrypted pairwise session rumor that may carry sender-key distribution.
    pub fn group_handle_incoming_session_event(
        &self,
        event_json: String,
        from_owner_pubkey_hex: String,
        from_sender_device_pubkey_hex: Option<String>,
    ) -> Result<Vec<GroupDecryptedResult>, NdrError> {
        let event: nostr::UnsignedEvent = serde_json::from_str(&event_json)?;
        let from_owner_pubkey =
            nostr_double_ratchet::utils::pubkey_from_hex(&from_owner_pubkey_hex)?;

        let from_sender_device_pubkey = match from_sender_device_pubkey_hex {
            Some(hex) => Some(nostr_double_ratchet::utils::pubkey_from_hex(&hex)?),
            None => Some(event.pubkey),
        };

        let decrypted = self.runtime.with_group_context(|_, group_manager, _| {
            group_manager.handle_incoming_session_event(
                &event,
                from_owner_pubkey,
                from_sender_device_pubkey,
            )
        });
        Ok(decrypted
            .into_iter()
            .map(group_decrypted_to_result)
            .collect())
    }

    /// Handle an incoming relay event that may be an encrypted one-to-many group outer event.
    pub fn group_handle_outer_event(
        &self,
        event_json: String,
    ) -> Result<Option<GroupDecryptedResult>, NdrError> {
        let event: nostr::Event = serde_json::from_str(&event_json)?;
        Ok(self.runtime.with_group_context(|_, group_manager, _| {
            group_manager
                .handle_outer_event(&event)
                .map(group_decrypted_to_result)
        }))
    }

    /// Send a delivery/read receipt for messages.
    pub fn send_receipt(
        &self,
        recipient_pubkey_hex: String,
        receipt_type: String,
        message_ids: Vec<String>,
        expires_at_seconds: Option<u64>,
    ) -> Result<Vec<String>, NdrError> {
        let recipient = nostr_double_ratchet::utils::pubkey_from_hex(&recipient_pubkey_hex)?;
        let options = expires_at_seconds.map(|expires_at| nostr_double_ratchet::SendOptions {
            expires_at: Some(expires_at),
            ttl_seconds: None,
        });
        Ok(self.runtime.session_manager().send_receipt(
            recipient,
            &receipt_type,
            message_ids,
            options,
        )?)
    }

    /// Send a typing indicator.
    pub fn send_typing(
        &self,
        recipient_pubkey_hex: String,
        expires_at_seconds: Option<u64>,
    ) -> Result<Vec<String>, NdrError> {
        let recipient = nostr_double_ratchet::utils::pubkey_from_hex(&recipient_pubkey_hex)?;
        let options = expires_at_seconds.map(|expires_at| nostr_double_ratchet::SendOptions {
            expires_at: Some(expires_at),
            ttl_seconds: None,
        });
        Ok(self
            .runtime
            .session_manager()
            .send_typing(recipient, options)?)
    }

    /// Send an emoji reaction (kind 7) to a specific message id.
    pub fn send_reaction(
        &self,
        recipient_pubkey_hex: String,
        message_id: String,
        emoji: String,
        expires_at_seconds: Option<u64>,
    ) -> Result<Vec<String>, NdrError> {
        let recipient = nostr_double_ratchet::utils::pubkey_from_hex(&recipient_pubkey_hex)?;
        let options = expires_at_seconds.map(|expires_at| nostr_double_ratchet::SendOptions {
            expires_at: Some(expires_at),
            ttl_seconds: None,
        });
        Ok(self
            .runtime
            .session_manager()
            .send_reaction(recipient, message_id, emoji, options)?)
    }

    /// Import a session state for a peer.
    pub fn import_session_state(
        &self,
        peer_pubkey_hex: String,
        state_json: String,
        device_id: Option<String>,
    ) -> Result<(), NdrError> {
        let peer_pubkey = nostr_double_ratchet::utils::pubkey_from_hex(&peer_pubkey_hex)?;
        let state: SessionState =
            nostr_double_ratchet::utils::deserialize_session_state(&state_json)?;
        self.runtime
            .import_session_state(peer_pubkey, device_id, state)?;
        Ok(())
    }

    /// Export the active session state for a peer.
    pub fn get_active_session_state(
        &self,
        peer_pubkey_hex: String,
    ) -> Result<Option<String>, NdrError> {
        let peer_pubkey = nostr_double_ratchet::utils::pubkey_from_hex(&peer_pubkey_hex)?;
        if let Some(state) = self
            .runtime
            .session_manager()
            .export_active_session_state(peer_pubkey)?
        {
            Ok(Some(nostr_double_ratchet::utils::serialize_session_state(
                &state,
            )?))
        } else {
            Ok(None)
        }
    }

    /// List peer owner pubkeys known from loaded state or persisted storage.
    pub fn known_peer_owner_pubkeys(&self) -> Vec<String> {
        self.runtime
            .session_manager()
            .known_peer_owner_pubkeys()
            .into_iter()
            .map(|pubkey| pubkey.to_hex())
            .collect()
    }

    /// Return the persisted user-record snapshot JSON for a peer owner, if present.
    pub fn get_stored_user_record_json(
        &self,
        peer_owner_pubkey_hex: String,
    ) -> Result<Option<String>, NdrError> {
        let peer_owner_pubkey =
            nostr_double_ratchet::utils::pubkey_from_hex(&peer_owner_pubkey_hex)?;
        Ok(self
            .runtime
            .session_manager()
            .get_stored_user_record_json(peer_owner_pubkey)?)
    }

    /// Return the tracked message-push author pubkeys for a peer owner.
    pub fn get_message_push_author_pubkeys(
        &self,
        peer_owner_pubkey_hex: String,
    ) -> Result<Vec<String>, NdrError> {
        let peer_owner_pubkey =
            nostr_double_ratchet::utils::pubkey_from_hex(&peer_owner_pubkey_hex)?;
        Ok(self
            .runtime
            .session_manager()
            .get_message_push_author_pubkeys(peer_owner_pubkey)
            .into_iter()
            .map(|pubkey| pubkey.to_hex())
            .collect())
    }

    /// Return pairwise session snapshots used for message-push routing for a peer owner.
    pub fn get_message_push_session_states(
        &self,
        peer_owner_pubkey_hex: String,
    ) -> Result<Vec<MessagePushSessionStateResult>, NdrError> {
        let peer_owner_pubkey =
            nostr_double_ratchet::utils::pubkey_from_hex(&peer_owner_pubkey_hex)?;
        self.runtime
            .session_manager()
            .get_message_push_session_states(peer_owner_pubkey)
            .into_iter()
            .map(|snapshot| {
                Ok(MessagePushSessionStateResult {
                    state_json: nostr_double_ratchet::utils::serialize_session_state(
                        &snapshot.state,
                    )?,
                    tracked_sender_pubkeys: snapshot
                        .tracked_sender_pubkeys
                        .into_iter()
                        .map(|pubkey| pubkey.to_hex())
                        .collect(),
                    has_receiving_capability: snapshot.has_receiving_capability,
                })
            })
            .collect()
    }

    /// Process a received Nostr event JSON.
    pub fn process_event(&self, event_json: String) -> Result<(), NdrError> {
        let event: nostr::Event = serde_json::from_str(&event_json)?;
        self.runtime.session_manager().process_received_event(event);
        Ok(())
    }

    /// Drain pending pubsub events from the internal queue.
    pub fn drain_events(&self) -> Result<Vec<PubSubEvent>, NdrError> {
        let mut events = Vec::new();

        for event in self.runtime.drain_events() {
            let pubsub_event = match event {
                SessionManagerEvent::Publish(unsigned) => PubSubEvent {
                    kind: "publish".to_string(),
                    subid: None,
                    filter_json: None,
                    event_json: Some(serde_json::to_string(&unsigned)?),
                    sender_pubkey_hex: None,
                    content: None,
                    event_id: None,
                },
                SessionManagerEvent::PublishSigned(signed) => PubSubEvent {
                    kind: "publish_signed".to_string(),
                    subid: None,
                    filter_json: None,
                    event_json: Some(serde_json::to_string(&signed)?),
                    sender_pubkey_hex: None,
                    content: None,
                    event_id: None,
                },
                SessionManagerEvent::PublishSignedForInnerEvent {
                    event,
                    inner_event_id,
                } => PubSubEvent {
                    kind: "publish_signed".to_string(),
                    subid: None,
                    filter_json: None,
                    event_json: Some(serde_json::to_string(&event)?),
                    sender_pubkey_hex: None,
                    content: None,
                    event_id: inner_event_id,
                },
                SessionManagerEvent::Subscribe { subid, filter_json } => PubSubEvent {
                    kind: "subscribe".to_string(),
                    subid: Some(subid),
                    filter_json: Some(filter_json),
                    event_json: None,
                    sender_pubkey_hex: None,
                    content: None,
                    event_id: None,
                },
                SessionManagerEvent::Unsubscribe(subid) => PubSubEvent {
                    kind: "unsubscribe".to_string(),
                    subid: Some(subid),
                    filter_json: None,
                    event_json: None,
                    sender_pubkey_hex: None,
                    content: None,
                    event_id: None,
                },
                SessionManagerEvent::DecryptedMessage {
                    sender,
                    content,
                    event_id,
                    ..
                } => PubSubEvent {
                    kind: "decrypted_message".to_string(),
                    subid: None,
                    filter_json: None,
                    event_json: None,
                    sender_pubkey_hex: Some(sender.to_hex()),
                    content: Some(content),
                    event_id,
                },
                SessionManagerEvent::ReceivedEvent(event) => PubSubEvent {
                    kind: "received_event".to_string(),
                    subid: None,
                    filter_json: None,
                    event_json: Some(serde_json::to_string(&event)?),
                    sender_pubkey_hex: None,
                    content: None,
                    event_id: None,
                },
            };
            events.push(pubsub_event);
        }

        Ok(events)
    }

    /// Get our device id.
    pub fn get_device_id(&self) -> String {
        self.runtime.get_device_id().to_string()
    }

    /// Get our public key as hex.
    pub fn get_our_pubkey_hex(&self) -> String {
        self.runtime.get_our_pubkey().to_hex()
    }

    /// Get owner public key as hex.
    pub fn get_owner_pubkey_hex(&self) -> String {
        self.runtime.get_owner_pubkey().to_hex()
    }

    /// Get total active sessions.
    pub fn get_total_sessions(&self) -> u64 {
        self.runtime.session_manager().get_total_sessions() as u64
    }
}

/// Parse a hex-encoded private key.
fn parse_private_key(hex_str: &str) -> Result<[u8; 32], NdrError> {
    let bytes = hex::decode(hex_str).map_err(|_| NdrError::InvalidKey("Invalid hex".into()))?;
    if bytes.len() != 32 {
        return Err(NdrError::InvalidKey("Private key must be 32 bytes".into()));
    }
    let mut arr = [0u8; 32];
    arr.copy_from_slice(&bytes);
    Ok(arr)
}

/// Parse a hex-encoded secret.
fn parse_secret(hex_str: &str) -> Result<[u8; 32], NdrError> {
    let bytes = hex::decode(hex_str).map_err(|_| NdrError::Serialization("Invalid hex".into()))?;
    if bytes.len() != 32 {
        return Err(NdrError::Serialization("Secret must be 32 bytes".into()));
    }
    let mut arr = [0u8; 32];
    arr.copy_from_slice(&bytes);
    Ok(arr)
}

fn ffi_group_data_to_group_data(group: FfiGroupData) -> GroupData {
    GroupData {
        id: group.id,
        name: group.name,
        description: group.description,
        picture: group.picture,
        members: group.members,
        admins: group.admins,
        created_at: group.created_at_ms,
        secret: group.secret,
        accepted: group.accepted,
    }
}

fn group_data_to_ffi_group_data(group: GroupData) -> FfiGroupData {
    FfiGroupData {
        id: group.id,
        name: group.name,
        description: group.description,
        picture: group.picture,
        members: group.members,
        admins: group.admins,
        created_at_ms: group.created_at,
        secret: group.secret,
        accepted: group.accepted,
    }
}

fn unsigned_event_id_string(event: &nostr::UnsignedEvent) -> String {
    event
        .id
        .as_ref()
        .map(std::string::ToString::to_string)
        .unwrap_or_default()
}

fn group_decrypted_to_result(event: GroupDecryptedEvent) -> GroupDecryptedResult {
    GroupDecryptedResult {
        group_id: event.group_id,
        sender_event_pubkey_hex: event.sender_event_pubkey.to_hex(),
        sender_device_pubkey_hex: event.sender_device_pubkey.to_hex(),
        sender_owner_pubkey_hex: event.sender_owner_pubkey.map(|pk| pk.to_hex()),
        outer_event_id: event.outer_event_id,
        outer_created_at: event.outer_created_at,
        key_id: event.key_id,
        message_number: event.message_number,
        inner_event_json: serde_json::to_string(&event.inner).unwrap_or_default(),
        inner_event_id: unsigned_event_id_string(&event.inner),
    }
}

uniffi::setup_scaffolding!();

/// Result of processing an invite response.
#[derive(uniffi::Record)]
pub struct InviteProcessResult {
    pub session: Arc<SessionHandle>,
    pub invitee_pubkey_hex: String,
    pub device_id: Option<String>,
    pub owner_pubkey_hex: Option<String>,
}

/// Result of sending a text message including stable inner id.
#[derive(uniffi::Record)]
pub struct SendTextResult {
    pub inner_id: String,
    pub outer_event_ids: Vec<String>,
}

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

    #[test]
    fn test_version() {
        let v = version();
        assert!(!v.is_empty());
        assert!(v.contains('.'));
    }

    #[test]
    fn test_keypair_generate_formats_hex() {
        let kp = generate_keypair();
        assert_eq!(kp.public_key_hex.len(), 64);
        assert_eq!(kp.private_key_hex.len(), 64);
        // Verify they're valid hex
        assert!(hex::decode(&kp.public_key_hex).is_ok());
        assert!(hex::decode(&kp.private_key_hex).is_ok());
    }

    #[test]
    fn test_derive_public_key_matches_generate() {
        let kp = generate_keypair();
        let pubkey = derive_public_key(kp.private_key_hex.clone()).unwrap();
        assert_eq!(pubkey, kp.public_key_hex);
    }

    #[test]
    fn test_invite_url_roundtrip() {
        let kp = generate_keypair();
        let invite = InviteHandle::create_new(kp.public_key_hex.clone(), None, None).unwrap();

        let url = invite.to_url("https://example.com".to_string()).unwrap();
        assert!(url.starts_with("https://example.com"));

        let restored = InviteHandle::from_url(url).unwrap();
        assert_eq!(
            invite.get_inviter_pubkey_hex(),
            restored.get_inviter_pubkey_hex()
        );
        assert_eq!(
            invite.get_shared_secret_hex(),
            restored.get_shared_secret_hex()
        );
    }

    #[test]
    fn test_invite_serialize_roundtrip() {
        let kp = generate_keypair();
        let invite =
            InviteHandle::create_new(kp.public_key_hex.clone(), Some("device1".into()), Some(5))
                .unwrap();

        let json = invite.serialize().unwrap();
        let restored = InviteHandle::deserialize(json).unwrap();

        assert_eq!(
            invite.get_inviter_pubkey_hex(),
            restored.get_inviter_pubkey_hex()
        );
        assert_eq!(
            invite.get_shared_secret_hex(),
            restored.get_shared_secret_hex()
        );
    }

    #[test]
    fn test_invite_accept_returns_session_and_event() {
        let inviter_kp = generate_keypair();
        let invitee_kp = generate_keypair();

        let invite =
            InviteHandle::create_new(inviter_kp.public_key_hex.clone(), None, None).unwrap();
        let url = invite.to_url("https://example.com".to_string()).unwrap();

        let invite_copy = InviteHandle::from_url(url).unwrap();
        let result = invite_copy
            .accept(
                invitee_kp.public_key_hex.clone(),
                invitee_kp.private_key_hex.clone(),
                None,
            )
            .unwrap();

        assert!(!result.response_event_json.is_empty());
        assert!(result.session.can_send());
    }

    #[test]
    fn test_invite_process_response_yields_working_session_pair() {
        let alice_kp = generate_keypair();
        let bob_kp = generate_keypair();

        let invite = InviteHandle::create_new(alice_kp.public_key_hex.clone(), None, None).unwrap();
        let accept = invite
            .accept(
                bob_kp.public_key_hex.clone(),
                bob_kp.private_key_hex.clone(),
                None,
            )
            .unwrap();

        let processed = invite
            .process_response(
                accept.response_event_json.clone(),
                alice_kp.private_key_hex.clone(),
            )
            .unwrap()
            .unwrap();

        // Bob sends first (initiator), Alice receives first (non-initiator)
        let bob_send = accept.session.send_text("hi".to_string()).unwrap();
        let alice_decrypt = processed
            .session
            .decrypt_event(bob_send.outer_event_json.clone())
            .unwrap();
        assert!(alice_decrypt.plaintext.contains("hi"));

        // After receiving, Alice should be able to send.
        assert!(processed.session.can_send());

        let alice_reply = processed.session.send_text("ok".to_string()).unwrap();
        let bob_decrypt = accept
            .session
            .decrypt_event(alice_reply.outer_event_json)
            .unwrap();
        assert!(bob_decrypt.plaintext.contains("ok"));
    }

    #[test]
    fn test_session_send_receive() {
        // Setup: create invite and accept it to get two linked sessions
        let alice_kp = generate_keypair();
        let bob_kp = generate_keypair();

        // Alice creates invite
        let invite = InviteHandle::create_new(alice_kp.public_key_hex.clone(), None, None).unwrap();
        let invite_json = invite.serialize().unwrap();

        // Bob accepts invite
        let bob_invite = InviteHandle::deserialize(invite_json).unwrap();
        let accept_result = bob_invite
            .accept(
                bob_kp.public_key_hex.clone(),
                bob_kp.private_key_hex.clone(),
                None,
            )
            .unwrap();

        let bob_session = accept_result.session;

        // Alice processes the response to create her session
        // For this test, we use the session's shared state to verify
        assert!(bob_session.can_send());

        // Bob sends a message
        let send_result = bob_session.send_text("Hello Alice!".to_string()).unwrap();
        assert!(!send_result.outer_event_json.is_empty());
    }

    #[test]
    fn test_session_state_roundtrip() {
        let alice_kp = generate_keypair();
        let bob_kp = generate_keypair();

        let invite = InviteHandle::create_new(alice_kp.public_key_hex.clone(), None, None).unwrap();
        let invite_json = invite.serialize().unwrap();

        let bob_invite = InviteHandle::deserialize(invite_json).unwrap();
        let accept_result = bob_invite
            .accept(
                bob_kp.public_key_hex.clone(),
                bob_kp.private_key_hex.clone(),
                None,
            )
            .unwrap();

        let session = accept_result.session;
        let state_json = session.state_json().unwrap();

        // Restore session from state
        let restored = SessionHandle::from_state_json(state_json).unwrap();
        assert_eq!(session.can_send(), restored.can_send());
    }

    #[test]
    fn test_group_send_event_tracks_sender_event_pubkey() {
        let kp = generate_keypair();
        let manager = SessionManagerHandle::new(
            kp.public_key_hex.clone(),
            kp.private_key_hex.clone(),
            kp.public_key_hex.clone(),
            None,
        )
        .unwrap();
        manager.init().unwrap();

        manager
            .group_upsert(FfiGroupData {
                id: "group-ffi-test".to_string(),
                name: "ffi".to_string(),
                description: None,
                picture: None,
                members: vec![kp.public_key_hex.clone()],
                admins: vec![kp.public_key_hex.clone()],
                created_at_ms: 1_700_000_000_000,
                secret: None,
                accepted: Some(true),
            })
            .unwrap();

        assert!(manager.group_known_sender_event_pubkeys().is_empty());

        let send = manager
            .group_send_event(
                "group-ffi-test".to_string(),
                14,
                "hello".to_string(),
                "[]".to_string(),
                Some(1_700_000_000_000),
            )
            .unwrap();
        assert!(!send.outer_event_json.is_empty());
        assert!(!send.inner_event_json.is_empty());
        assert!(!send.outer_event_id.is_empty());
        assert!(!send.inner_event_id.is_empty());

        let sender_event_pubkeys = manager.group_known_sender_event_pubkeys();
        assert_eq!(
            sender_event_pubkeys.len(),
            0,
            "local sender-event pubkeys should be filtered from subscription lists"
        );
    }

    #[test]
    fn test_group_create_returns_group_and_metadata_rumor() {
        let kp = generate_keypair();
        let manager = SessionManagerHandle::new(
            kp.public_key_hex.clone(),
            kp.private_key_hex.clone(),
            kp.public_key_hex.clone(),
            None,
        )
        .unwrap();
        manager.init().unwrap();

        let created = manager
            .group_create(
                "ffi-created".to_string(),
                vec![kp.public_key_hex.clone()],
                Some(true),
                Some(1_700_000_123_000),
            )
            .unwrap();

        assert_eq!(created.group.name, "ffi-created");
        assert!(created.group.members.contains(&kp.public_key_hex));
        assert!(created.metadata_rumor_json.is_some());
        assert!(created.fanout.enabled);
    }

    #[test]
    fn test_send_rumor_json_preserves_inner_id() {
        let alice = generate_keypair();
        let bob = generate_keypair();
        let sender_device = generate_keypair();

        let manager = SessionManagerHandle::new(
            alice.public_key_hex.clone(),
            alice.private_key_hex.clone(),
            alice.public_key_hex.clone(),
            None,
        )
        .unwrap();
        manager.init().unwrap();

        let our_next = nostr::Keys::generate();
        let state = SessionState {
            root_key: [7u8; 32],
            their_current_nostr_public_key: Some(
                nostr_double_ratchet::utils::pubkey_from_hex(&bob.public_key_hex).unwrap(),
            ),
            their_next_nostr_public_key: None,
            our_current_nostr_key: None,
            our_next_nostr_key: nostr_double_ratchet::SerializableKeyPair {
                public_key: our_next.public_key(),
                private_key: our_next.secret_key().secret_bytes(),
            },
            receiving_chain_key: None,
            sending_chain_key: Some([9u8; 32]),
            sending_chain_message_number: 0,
            receiving_chain_message_number: 0,
            previous_sending_chain_message_count: 0,
            skipped_keys: std::collections::HashMap::new(),
        };

        manager
            .import_session_state(
                bob.public_key_hex.clone(),
                nostr_double_ratchet::utils::serialize_session_state(&state).unwrap(),
                Some("bob-device".to_string()),
            )
            .unwrap();

        let rumor = nostr::EventBuilder::new(nostr::Kind::Custom(14), "raw rumor")
            .tags(vec![
                nostr::Tag::parse(&["l".to_string(), "group-ffi-test".to_string()]).unwrap(),
                nostr::Tag::parse(&["ms".to_string(), "1700000000000".to_string()]).unwrap(),
            ])
            .custom_created_at(nostr::Timestamp::from(1_700_000_000))
            .build(
                nostr_double_ratchet::utils::pubkey_from_hex(&sender_device.public_key_hex)
                    .unwrap(),
            );
        let rumor_json = serde_json::to_string(&rumor).unwrap();

        let send = manager
            .send_rumor_json(bob.public_key_hex.clone(), rumor_json)
            .unwrap();

        assert_eq!(
            send.inner_id,
            rumor.id.as_ref().map(ToString::to_string).unwrap()
        );
    }

    #[test]
    fn test_is_dr_message() {
        let kp = generate_keypair();
        let invite = InviteHandle::create_new(kp.public_key_hex.clone(), None, None).unwrap();

        let bob_kp = generate_keypair();
        let accept_result = invite
            .accept(
                bob_kp.public_key_hex.clone(),
                bob_kp.private_key_hex.clone(),
                None,
            )
            .unwrap();

        let session = accept_result.session;
        let send_result = session.send_text("test".to_string()).unwrap();

        // DR message should return true
        assert!(session.is_dr_message(send_result.outer_event_json));

        // Non-DR message should return false
        let non_dr_event = serde_json::json!({
            "id": "0000000000000000000000000000000000000000000000000000000000000000",
            "pubkey": "0000000000000000000000000000000000000000000000000000000000000000",
            "created_at": 0,
            "kind": 1,
            "tags": [],
            "content": "test",
            "sig": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
        });
        assert!(!session.is_dr_message(non_dr_event.to_string()));
    }

    #[test]
    fn test_app_keys_labels_roundtrip() {
        let owner = generate_keypair();
        let laptop = generate_keypair();
        let phone = generate_keypair();

        let event_json = create_signed_app_keys_event(
            owner.public_key_hex.clone(),
            owner.private_key_hex.clone(),
            vec![
                FfiDeviceEntry {
                    identity_pubkey_hex: laptop.public_key_hex.clone(),
                    created_at: 1_700_000_000,
                    device_label: Some("Sirius MacBook".to_string()),
                    client_label: Some("Iris Chat Desktop".to_string()),
                },
                FfiDeviceEntry {
                    identity_pubkey_hex: phone.public_key_hex.clone(),
                    created_at: 1_700_000_100,
                    device_label: Some("Linked device".to_string()),
                    client_label: Some("Iris Chat Mobile".to_string()),
                },
            ],
        )
        .unwrap();

        let parsed =
            parse_app_keys_event(event_json.clone(), Some(owner.private_key_hex.clone())).unwrap();
        let resolved =
            resolve_latest_app_keys_devices(vec![event_json], Some(owner.private_key_hex)).unwrap();

        for devices in [parsed, resolved] {
            assert_eq!(devices.len(), 2);

            let laptop_entry = devices
                .iter()
                .find(|entry| entry.identity_pubkey_hex == laptop.public_key_hex)
                .unwrap();
            assert_eq!(laptop_entry.device_label.as_deref(), Some("Sirius MacBook"));
            assert_eq!(
                laptop_entry.client_label.as_deref(),
                Some("Iris Chat Desktop")
            );

            let phone_entry = devices
                .iter()
                .find(|entry| entry.identity_pubkey_hex == phone.public_key_hex)
                .unwrap();
            assert_eq!(phone_entry.device_label.as_deref(), Some("Linked device"));
            assert_eq!(
                phone_entry.client_label.as_deref(),
                Some("Iris Chat Mobile")
            );
        }
    }
}
