use crate::conversation::{
    Channel, ChannelInfo, Conversation, Group, InboxMessage, NewMessage, Thread, ThreadMessage,
};
use crate::db::Db;
use crate::error::{Result, SdkError};
use crate::secret::{DecryptionKeys, Seed, SigningKey};
use crate::types::{BlockRef, Pubkey};
use chrono::{DateTime, Utc};
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::mpsc;
use zeroize::Zeroizing;

pub const MAX_GROUP_MEMBERS: usize = 20;

fn rand_nonce() -> [u8; 12] {
    let mut nonce = [0u8; 12];
    getrandom::fill(&mut nonce).expect("OS RNG");
    nonce
}

fn refresh_message_gaps(messages: &mut [ThreadMessage], has_msg: impl Fn(BlockRef) -> bool) {
    for msg in messages {
        msg.has_gap = (msg.reply_to != BlockRef::ZERO && !has_msg(msg.reply_to))
            || (msg.continues != BlockRef::ZERO && !has_msg(msg.continues));
    }
}

fn reindex(index: &mut HashMap<BlockRef, usize>, removed: usize) {
    index.retain(|_, v| {
        if *v > removed {
            *v -= 1;
        }
        true
    });
}

pub struct Session {
    signing: Option<Arc<SigningKey>>,
    pubkey: Pubkey,
    pub my_ss58: String,
    seed: Option<Zeroizing<[u8; 32]>>,
    view_scalar: Zeroizing<[u8; 32]>,
    pub node_url: crate::types::NodeUrl,
    pub chain_info: crate::extrinsic::ChainInfo,
    pub block_number: u64,
    pub inbox: Vec<InboxMessage>,
    pub outbox: Vec<InboxMessage>,
    pub threads: Vec<Thread>,
    pub channels: Vec<Channel>,
    pub groups: Vec<Group>,
    pub db: Db,
    thread_index: HashMap<BlockRef, usize>,
    channel_index: HashMap<BlockRef, usize>,
    group_index: HashMap<BlockRef, usize>,
    pub known_channels: Vec<ChannelInfo>,
    known_channel_index: HashMap<BlockRef, usize>,
    pub peer_pubkeys: HashMap<String, Pubkey>,
    pub token_symbol: String,
    pub token_decimals: u32,
    pub balance: Option<u128>,
    pub has_mirror: bool,
}

impl Session {
    pub fn new(
        signing: SigningKey,
        seed: Zeroizing<[u8; 32]>,
        keep_seed: bool,
        node_url: crate::types::NodeUrl,
        chain_info: crate::extrinsic::ChainInfo,
        db: Db,
    ) -> Self {
        let pubkey = signing.public_key();
        let my_ss58 = crate::util::ss58_from_pubkey(&pubkey);
        let view_scalar = Zeroizing::new(
            *samp::sr25519_signing_scalar(&samp::Seed::from_bytes(*seed)).expose_secret(),
        );
        let signing = if keep_seed {
            Some(Arc::new(signing))
        } else {
            drop(signing);
            None
        };
        let seed = if keep_seed { Some(seed) } else { None };
        Self {
            signing,
            pubkey,
            my_ss58,
            seed,
            view_scalar,
            node_url,
            chain_info,
            block_number: 0,
            inbox: Vec::new(),
            outbox: Vec::new(),
            threads: Vec::new(),
            channels: Vec::new(),
            groups: Vec::new(),
            db,
            thread_index: HashMap::new(),
            channel_index: HashMap::new(),
            group_index: HashMap::new(),
            known_channels: Vec::new(),
            known_channel_index: HashMap::new(),
            peer_pubkeys: HashMap::new(),
            token_symbol: String::new(),
            token_decimals: 0,
            balance: None,
            has_mirror: false,
        }
    }

    pub async fn start(
        seed: &[u8; 32],
        node_url: &str,
        wallet_name: &str,
        keep_seed: bool,
    ) -> Result<(Self, mpsc::Receiver<crate::event::Event>)> {
        Self::start_with_mirrors(seed, node_url, wallet_name, &[], keep_seed).await
    }

    pub async fn start_with_mirrors(
        seed: &[u8; 32],
        node_url: &str,
        wallet_name: &str,
        mirror_urls: &[String],
        keep_seed: bool,
    ) -> Result<(Self, mpsc::Receiver<crate::event::Event>)> {
        let signing = Seed::from_bytes(*seed).derive_signing_key();
        let my_pubkey = signing.public_key();

        let chain_info = crate::extrinsic::fetch_chain_info(node_url).await?;

        let (symbol, decimals) = crate::extrinsic::fetch_token_info(node_url)
            .await
            .unwrap_or_else(|_| ("TAO".into(), 9));

        let db = Db::open(
            wallet_name,
            seed,
            chain_info.chain_params.genesis_hash().as_bytes(),
        )?;

        let mut session = Self::new(
            signing,
            Zeroizing::new(*seed),
            keep_seed,
            crate::types::NodeUrl::parse(node_url).map_err(|e| SdkError::Other(e.to_string()))?,
            chain_info,
            db,
        );
        session.token_symbol = symbol;
        session.token_decimals = decimals;
        session.load_from_db();

        if let Ok(bal) = crate::extrinsic::fetch_balance(
            node_url,
            &my_pubkey,
            &session.chain_info.account_storage,
        )
        .await
        {
            session.balance = Some(bal);
        }

        let (tx, rx) = mpsc::channel();

        {
            let url = node_url.to_string();
            let etx = tx.clone();
            let keys = session.decryption_keys();
            tokio::spawn(async move {
                crate::chain::subscribe_blocks(&url, my_pubkey, keys, etx).await;
            });
        }

        session.has_mirror = !mirror_urls.is_empty();
        if session.has_mirror {
            let subscribed: Vec<BlockRef> =
                session.channels.iter().map(|c| c.channel_ref).collect();
            let urls: Vec<String> = mirror_urls.iter().map(|u| u.to_string()).collect();
            let node = node_url.to_string();
            let keys = session.decryption_keys();
            let pk = my_pubkey;
            let etx = tx.clone();
            let chain_name = session.chain_info.name.clone();
            let ss58_prefix = session.chain_info.ss58_prefix;
            tokio::spawn(async move {
                crate::mirror::sync(
                    urls,
                    &node,
                    &chain_name,
                    ss58_prefix,
                    &keys,
                    &pk,
                    subscribed,
                    0,
                    etx,
                )
                .await;
            });
        }

        Ok((session, rx))
    }

    pub fn pubkey(&self) -> Pubkey {
        self.pubkey
    }

    pub fn signing(&self) -> Option<Arc<SigningKey>> {
        self.signing.as_ref().map(Arc::clone)
    }

    pub fn view_scalar(&self) -> samp::ViewScalar {
        samp::ViewScalar::from_bytes(*self.view_scalar)
    }

    pub fn decryption_keys(&self) -> DecryptionKeys {
        DecryptionKeys::new(*self.view_scalar, self.seed.as_deref().copied())
    }

    pub fn cached_seed(&self) -> Option<&[u8; 32]> {
        self.seed.as_deref()
    }

    pub fn ss58(&self) -> &str {
        &self.my_ss58
    }

    pub fn load_from_db(&mut self) {
        self.peer_pubkeys = self.db.load_all_peers();
        let (inbox, outbox) = self.db.load_inbox();
        self.inbox = inbox;
        self.outbox = outbox;

        for (thread_ref, peer_ss58, messages) in self.db.load_threads() {
            let i = self.threads.len();
            self.thread_index.insert(thread_ref, i);
            let peer_pubkey = self.db.get_peer_pubkey(&peer_ss58).unwrap_or(Pubkey::ZERO);
            if peer_pubkey != Pubkey::ZERO {
                self.peer_pubkeys.insert(peer_ss58.clone(), peer_pubkey);
            }
            let msg_count = messages.len();
            self.threads.push(Thread {
                thread_ref,
                peer_ss58,
                peer_pubkey,
                messages,
                draft: String::new(),
                last_read: msg_count,
            });
            self.refresh_gaps(crate::db::ConversationKind::Thread, i);
        }

        for (channel_ref, name, description, creator_ss58, messages) in self.db.load_channels() {
            let i = self.channels.len();
            let msg_count = messages.len();
            self.channel_index.insert(channel_ref, i);
            self.channels.push(Channel {
                name,
                description,
                creator_ss58,
                channel_ref,
                messages,
                draft: String::new(),
                last_read: msg_count,
            });
            self.refresh_gaps(crate::db::ConversationKind::Channel, i);
        }

        for (channel_ref, name, description, creator_ss58) in self.db.load_known_channels() {
            if self.known_channel_index.contains_key(&channel_ref) {
                continue;
            }
            let i = self.known_channels.len();
            self.known_channel_index.insert(channel_ref, i);
            self.known_channels.push(ChannelInfo {
                name,
                description,
                creator_ss58,
                channel_ref,
            });
        }

        let me = self.pubkey();
        for (group_ref, creator_pubkey, members) in self.db.load_groups() {
            for member in &members {
                if *member == me {
                    continue;
                }
                let ss58 = crate::util::ss58_short(member);
                self.peer_pubkeys.insert(ss58.clone(), *member);
                self.db.upsert_peer(&ss58, member);
            }
            let i = self.groups.len();
            let messages = self.db.load_group_messages(group_ref);
            let msg_count = messages.len();
            self.group_index.insert(group_ref, i);
            self.groups.push(Group {
                creator_pubkey,
                group_ref,
                members,
                messages,
                draft: String::new(),
                last_read: msg_count,
            });
            self.refresh_gaps(crate::db::ConversationKind::Group, i);
        }

        for (kind, bref, body) in self.db.load_drafts() {
            if body.is_empty() {
                continue;
            }
            match kind {
                crate::db::ConversationKind::Thread => {
                    if let Some(t) = self.threads.iter_mut().find(|t| t.thread_ref == bref) {
                        t.draft = body;
                    }
                }
                crate::db::ConversationKind::Channel => {
                    if let Some(c) = self.channels.iter_mut().find(|c| c.channel_ref == bref) {
                        c.draft = body;
                    }
                }
                crate::db::ConversationKind::Group => {
                    if let Some(g) = self.groups.iter_mut().find(|g| g.group_ref == bref) {
                        g.draft = body;
                    }
                }
                crate::db::ConversationKind::Inbox => {}
            }
        }
    }

    pub fn add_inbox_message(
        &mut self,
        sender: Pubkey,
        recipient: Pubkey,
        timestamp: DateTime<Utc>,
        body: String,
        content_type: u8,
        block_ref: BlockRef,
    ) {
        let is_mine = sender == self.pubkey();
        let peer = if is_mine { recipient } else { sender };
        let peer_ss58 = crate::util::ss58_short(&peer);
        self.peer_pubkeys.insert(peer_ss58.clone(), peer);
        self.db.upsert_peer(&peer_ss58, &peer);

        let (block_number, ext_index) = (block_ref.block().get(), block_ref.index().get());
        if block_number > 0 {
            let already = if is_mine {
                self.outbox
                    .iter()
                    .any(|m| m.block_number == block_number && m.ext_index == ext_index)
            } else {
                self.inbox
                    .iter()
                    .any(|m| m.block_number == block_number && m.ext_index == ext_index)
            };
            if already {
                return;
            }
        }
        let msg = InboxMessage {
            peer_ss58,
            timestamp,
            body,
            content_type,
            is_mine,
            block_number,
            ext_index,
        };
        self.db.insert_inbox(&msg);
        let target = if is_mine {
            &mut self.outbox
        } else {
            &mut self.inbox
        };
        target.push(msg);
        target.sort_by_key(|m| (m.block_number, m.ext_index));
    }

    pub fn add_thread_message(
        &mut self,
        sender: Pubkey,
        recipient: Pubkey,
        mut thread_ref: BlockRef,
        msg: NewMessage,
    ) {
        let is_mine = sender == self.pubkey();
        let peer = if is_mine { recipient } else { sender };
        let peer_ss58 = if is_mine {
            crate::util::ss58_short(&recipient)
        } else {
            msg.sender_ss58.clone()
        };
        self.peer_pubkeys.insert(peer_ss58.clone(), peer);
        self.db.upsert_peer(&peer_ss58, &peer);

        if self.db.has_message_at(
            crate::db::ConversationKind::Thread,
            BlockRef::from_parts(msg.block_number, msg.ext_index),
        ) {
            return;
        }

        if thread_ref == BlockRef::ZERO {
            thread_ref = BlockRef::from_parts(msg.block_number, msg.ext_index);
        }

        let idx = if let Some(&i) = self.thread_index.get(&thread_ref) {
            i
        } else if let Some(&i) = self.thread_index.get(&BlockRef::ZERO) {
            if self.threads[i].peer_pubkey == peer {
                self.thread_index.remove(&BlockRef::ZERO);
                self.thread_index.insert(thread_ref, i);
                self.threads[i].thread_ref = thread_ref;
                i
            } else {
                let i = self.threads.len();
                self.thread_index.insert(thread_ref, i);
                self.threads.push(Thread {
                    thread_ref,
                    peer_ss58: peer_ss58.clone(),
                    peer_pubkey: peer,
                    messages: Vec::new(),
                    draft: String::new(),
                    last_read: 0,
                });
                i
            }
        } else {
            let i = self.threads.len();
            self.thread_index.insert(thread_ref, i);
            self.threads.push(Thread {
                thread_ref,
                peer_ss58: peer_ss58.clone(),
                peer_pubkey: peer,
                messages: Vec::new(),
                draft: String::new(),
                last_read: 0,
            });
            i
        };

        let has_gap = self.db.has_gap(
            crate::db::ConversationKind::Thread,
            msg.reply_to,
            msg.continues,
        );

        let tm = ThreadMessage::from_new(msg, is_mine, has_gap);

        self.db
            .insert_thread_message(thread_ref, &peer_ss58, &tm, tm.block_number, tm.ext_index);
        self.threads[idx].messages.push(tm);
        self.threads[idx]
            .messages
            .sort_by_key(|m| (m.block_number, m.ext_index));
    }

    pub fn add_channel_message(&mut self, channel_ref: BlockRef, msg: NewMessage) {
        if self.db.has_message_at(
            crate::db::ConversationKind::Channel,
            BlockRef::from_parts(msg.block_number, msg.ext_index),
        ) {
            return;
        }
        let idx = match self.channel_index.get(&channel_ref) {
            Some(&i) => i,
            None => return,
        };

        let is_mine = msg.sender_ss58 == crate::util::ss58_short(&self.pubkey());

        let has_gap = self.db.has_gap(
            crate::db::ConversationKind::Channel,
            msg.reply_to,
            msg.continues,
        );

        let tm = ThreadMessage::from_new(msg, is_mine, has_gap);

        self.db.insert_threaded_message(
            crate::db::ConversationKind::Channel,
            channel_ref,
            &tm,
            tm.block_number,
            tm.ext_index,
        );
        self.channels[idx].messages.push(tm);
        self.channels[idx]
            .messages
            .sort_by_key(|m| (m.block_number, m.ext_index));
    }

    pub fn discover_channel(
        &mut self,
        name: String,
        description: String,
        creator_ss58: String,
        channel_ref: BlockRef,
    ) {
        if let Some(&idx) = self.channel_index.get(&channel_ref) {
            self.db
                .update_channel_meta(channel_ref, &name, &description, &creator_ss58);
            self.channels[idx].name = name;
            self.channels[idx].description = description;
            self.channels[idx].creator_ss58 = creator_ss58;
            return;
        }
        if let Some(&idx) = self.channel_index.get(&BlockRef::ZERO)
            && self.channels[idx].name == name
        {
            self.channel_index.remove(&BlockRef::ZERO);
            self.channel_index.insert(channel_ref, idx);
            self.channels[idx].channel_ref = channel_ref;
            self.db
                .insert_channel(channel_ref, &name, &description, &creator_ss58);
            self.db
                .insert_known_channel(channel_ref, &name, &description, &creator_ss58);
            self.channels[idx].description = description.clone();
            self.channels[idx].creator_ss58 = creator_ss58.clone();
            let ki = self.known_channels.len();
            self.known_channel_index.insert(channel_ref, ki);
            self.known_channels.push(ChannelInfo {
                name,
                description,
                creator_ss58,
                channel_ref,
            });
            return;
        }
        if let Some(&idx) = self.known_channel_index.get(&channel_ref) {
            self.db
                .update_known_channel_meta(channel_ref, &name, &description, &creator_ss58);
            self.known_channels[idx].name = name;
            self.known_channels[idx].description = description;
            self.known_channels[idx].creator_ss58 = creator_ss58;
            return;
        }
        let i = self.known_channels.len();
        self.known_channel_index.insert(channel_ref, i);
        self.db
            .insert_known_channel(channel_ref, &name, &description, &creator_ss58);
        self.known_channels.push(ChannelInfo {
            name,
            description,
            creator_ss58,
            channel_ref,
        });
    }

    pub fn subscribe_channel(&mut self, channel_ref: BlockRef) -> usize {
        if let Some(&idx) = self.channel_index.get(&channel_ref) {
            return idx;
        }
        let (name, description, creator_ss58) =
            if let Some(&ki) = self.known_channel_index.get(&channel_ref) {
                let info = &self.known_channels[ki];
                (
                    info.name.clone(),
                    info.description.clone(),
                    info.creator_ss58.clone(),
                )
            } else {
                ("Loading...".into(), String::new(), String::new())
            };
        let i = self.channels.len();
        self.channel_index.insert(channel_ref, i);
        self.db
            .insert_channel(channel_ref, &name, &description, &creator_ss58);
        self.channels.push(Channel {
            name,
            description,
            creator_ss58,
            channel_ref,
            messages: Vec::new(),
            draft: String::new(),
            last_read: 0,
        });
        i
    }

    pub fn is_subscribed(&self, channel_ref: &BlockRef) -> bool {
        self.channel_index.contains_key(channel_ref)
    }

    pub fn channel_idx(&self, channel_ref: &BlockRef) -> Option<usize> {
        self.channel_index.get(channel_ref).copied()
    }

    pub fn unsubscribe_channel(&mut self, idx: usize) -> Option<String> {
        let channel = self.channels.get(idx)?;
        let name = channel.name.clone();
        let channel_ref = channel.channel_ref;
        self.channel_index.remove(&channel_ref);
        self.db.delete_channel(channel_ref);
        self.channels.remove(idx);
        reindex(&mut self.channel_index, idx);
        Some(name)
    }

    pub fn create_pending_group(&mut self, creator_pubkey: Pubkey, members: Vec<Pubkey>) -> usize {
        let i = self.groups.len();
        self.group_index.insert(BlockRef::ZERO, i);
        self.groups.push(Group {
            creator_pubkey,
            group_ref: BlockRef::ZERO,
            members,
            messages: Vec::new(),
            draft: String::new(),
            last_read: 0,
        });
        i
    }

    pub fn create_pending_channel(&mut self, name: String, creator_ss58: String) -> usize {
        let i = self.channels.len();
        self.channel_index.insert(BlockRef::ZERO, i);
        self.channels.push(Channel {
            name,
            description: String::new(),
            creator_ss58,
            channel_ref: BlockRef::ZERO,
            messages: Vec::new(),
            draft: String::new(),
            last_read: 0,
        });
        i
    }

    pub fn refresh_gaps(&mut self, kind: crate::db::ConversationKind, idx: usize) {
        let messages: Option<&mut Vec<ThreadMessage>> = match kind {
            crate::db::ConversationKind::Thread => {
                self.threads.get_mut(idx).map(|t| &mut t.messages)
            }
            crate::db::ConversationKind::Channel => {
                self.channels.get_mut(idx).map(|c| &mut c.messages)
            }
            crate::db::ConversationKind::Group => self.groups.get_mut(idx).map(|g| &mut g.messages),
            crate::db::ConversationKind::Inbox => None,
        };
        if let Some(messages) = messages {
            refresh_message_gaps(messages, |br| self.db.has_message_at(kind, br));
        }
    }

    pub fn discover_group(
        &mut self,
        creator_pubkey: Pubkey,
        group_ref: BlockRef,
        members: Vec<Pubkey>,
    ) {
        let me = self.pubkey();
        for member in &members {
            if *member == me {
                continue;
            }
            let ss58 = crate::util::ss58_short(member);
            self.peer_pubkeys.insert(ss58.clone(), *member);
            self.db.upsert_peer(&ss58, member);
        }
        if self.group_index.contains_key(&group_ref) {
            return;
        }
        if let Some(&idx) = self.group_index.get(&BlockRef::ZERO)
            && self.groups[idx].members == members
        {
            self.group_index.remove(&BlockRef::ZERO);
            self.group_index.insert(group_ref, idx);
            self.groups[idx].group_ref = group_ref;
            self.groups[idx].creator_pubkey = creator_pubkey;
            return;
        }
        let i = self.groups.len();
        self.group_index.insert(group_ref, i);
        self.groups.push(Group {
            creator_pubkey,
            group_ref,
            members,
            messages: Vec::new(),
            draft: String::new(),
            last_read: 0,
        });
    }

    pub fn add_group_message(&mut self, group_ref: BlockRef, msg: NewMessage) {
        if self.db.has_message_at(
            crate::db::ConversationKind::Group,
            BlockRef::from_parts(msg.block_number, msg.ext_index),
        ) {
            return;
        }
        let idx = match self.group_index.get(&group_ref) {
            Some(&i) => i,
            None => return,
        };

        let is_mine = msg.sender_ss58 == crate::util::ss58_short(&self.pubkey());

        let has_gap = self.db.has_gap(
            crate::db::ConversationKind::Group,
            msg.reply_to,
            msg.continues,
        );

        let tm = ThreadMessage::from_new(msg, is_mine, has_gap);

        self.db.insert_threaded_message(
            crate::db::ConversationKind::Group,
            group_ref,
            &tm,
            tm.block_number,
            tm.ext_index,
        );
        self.groups[idx].messages.push(tm);
        self.groups[idx]
            .messages
            .sort_by_key(|m| (m.block_number, m.ext_index));
    }

    pub fn create_thread(&mut self, pubkey: Pubkey) -> Result<usize> {
        if pubkey == self.pubkey() {
            return Err(SdkError::Other("Cannot message yourself".into()));
        }
        let peer_ss58 = crate::util::ss58_short(&pubkey);
        self.peer_pubkeys.insert(peer_ss58.clone(), pubkey);
        self.db.upsert_peer(&peer_ss58, &pubkey);

        let idx = self.threads.len();
        self.thread_index.insert(BlockRef::ZERO, idx);
        self.threads.push(Thread {
            thread_ref: BlockRef::ZERO,
            peer_ss58,
            peer_pubkey: pubkey,
            messages: Vec::new(),
            draft: String::new(),
            last_read: 0,
        });

        Ok(idx)
    }

    pub fn known_contacts(&self) -> Vec<(String, Pubkey)> {
        self.peer_pubkeys
            .iter()
            .map(|(ss58, pk)| (ss58.clone(), *pk))
            .collect()
    }

    pub fn build_public_message(
        &self,
        recipient: &Pubkey,
        body: &crate::types::MessageBody,
    ) -> Result<samp::RemarkBytes> {
        Ok(samp::encode_public(recipient, body.as_str()))
    }

    pub fn build_encrypted_message(
        &self,
        seed: &[u8; 32],
        recipient: &Pubkey,
        body: &crate::types::MessageBody,
    ) -> Result<samp::RemarkBytes> {
        let nonce = samp::Nonce::from_bytes(rand_nonce());
        let sender = samp::Seed::from_bytes(*seed);
        let plaintext = samp::Plaintext::from_bytes(body.as_str().as_bytes().to_vec());
        let encrypted = samp::encrypt(&plaintext, recipient, &nonce, &sender)
            .map_err(|e| SdkError::Encryption(e.to_string()))?;
        let vt = samp::compute_view_tag(&sender, recipient, &nonce)
            .map_err(|e| SdkError::Encryption(e.to_string()))?;
        Ok(samp::encode_encrypted(
            samp::ContentType::Encrypted,
            vt,
            &nonce,
            &encrypted,
        ))
    }

    pub fn build_thread_root(
        &self,
        seed: &[u8; 32],
        recipient: &Pubkey,
        body: &crate::types::MessageBody,
    ) -> Result<samp::RemarkBytes> {
        let nonce = samp::Nonce::from_bytes(rand_nonce());
        let sender = samp::Seed::from_bytes(*seed);
        let plaintext = samp::Plaintext::from_bytes(samp::encode_thread_content(
            BlockRef::ZERO,
            BlockRef::ZERO,
            BlockRef::ZERO,
            body.as_str().as_bytes(),
        ));
        let encrypted = samp::encrypt(&plaintext, recipient, &nonce, &sender)
            .map_err(|e| SdkError::Encryption(e.to_string()))?;
        let vt = samp::compute_view_tag(&sender, recipient, &nonce)
            .map_err(|e| SdkError::Encryption(e.to_string()))?;
        Ok(samp::encode_encrypted(
            samp::ContentType::Thread,
            vt,
            &nonce,
            &encrypted,
        ))
    }

    pub fn build_thread_reply(
        &self,
        seed: &[u8; 32],
        thread_idx: usize,
        body: &crate::types::MessageBody,
    ) -> Result<samp::RemarkBytes> {
        let thread = self
            .threads
            .get(thread_idx)
            .ok_or_else(|| SdkError::NotFound("Thread not found".into()))?;
        let nonce = samp::Nonce::from_bytes(rand_nonce());
        let sender = samp::Seed::from_bytes(*seed);
        let plaintext = samp::Plaintext::from_bytes(samp::encode_thread_content(
            thread.thread_ref,
            thread.last_ref(),
            thread.my_last_ref(),
            body.as_str().as_bytes(),
        ));
        let encrypted = samp::encrypt(&plaintext, &thread.peer_pubkey, &nonce, &sender)
            .map_err(|e| SdkError::Encryption(e.to_string()))?;
        let vt = samp::compute_view_tag(&sender, &thread.peer_pubkey, &nonce)
            .map_err(|e| SdkError::Encryption(e.to_string()))?;
        Ok(samp::encode_encrypted(
            samp::ContentType::Thread,
            vt,
            &nonce,
            &encrypted,
        ))
    }

    pub fn build_channel_create(
        &self,
        name: &samp::ChannelName,
        description: &samp::ChannelDescription,
    ) -> Result<samp::RemarkBytes> {
        Ok(samp::encode_channel_create(name, description))
    }

    pub fn build_channel_message(
        &self,
        channel_idx: usize,
        body: &crate::types::MessageBody,
    ) -> Result<samp::RemarkBytes> {
        let channel = self
            .channels
            .get(channel_idx)
            .ok_or_else(|| SdkError::NotFound("Channel not found".into()))?;
        Ok(samp::encode_channel_msg(
            channel.channel_ref,
            channel.last_ref(),
            channel.my_last_ref(),
            body.as_str(),
        ))
    }

    pub fn build_group_create(
        &self,
        seed: &[u8; 32],
        members: &[Pubkey],
        body: &crate::types::MessageBody,
    ) -> Result<samp::RemarkBytes> {
        if members.len() > MAX_GROUP_MEMBERS {
            return Err(SdkError::Other(format!(
                "Group too large: max {MAX_GROUP_MEMBERS} members supported"
            )));
        }
        let nonce = samp::Nonce::from_bytes(rand_nonce());
        let sender = samp::Seed::from_bytes(*seed);
        let mut body_bytes = samp::encode_group_members(members);
        body_bytes.extend_from_slice(body.as_str().as_bytes());
        let plaintext = samp::Plaintext::from_bytes(samp::encode_thread_content(
            BlockRef::ZERO,
            BlockRef::ZERO,
            BlockRef::ZERO,
            &body_bytes,
        ));
        let (eph_pubkey, capsules, ciphertext) =
            samp::encrypt_for_group(&plaintext, members, &nonce, &sender)
                .map_err(|e| SdkError::Encryption(e.to_string()))?;
        Ok(samp::encode_group(
            &nonce,
            &eph_pubkey,
            &capsules,
            &ciphertext,
        ))
    }

    pub fn build_group_message(
        &self,
        seed: &[u8; 32],
        group_idx: usize,
        body: &crate::types::MessageBody,
    ) -> Result<samp::RemarkBytes> {
        let group = self
            .groups
            .get(group_idx)
            .ok_or_else(|| SdkError::NotFound("Group not found".into()))?;
        let nonce = samp::Nonce::from_bytes(rand_nonce());
        let sender = samp::Seed::from_bytes(*seed);
        let plaintext = samp::Plaintext::from_bytes(samp::encode_thread_content(
            group.group_ref,
            group.last_ref(),
            group.my_last_ref(),
            body.as_str().as_bytes(),
        ));
        let (eph_pubkey, capsules, ciphertext) =
            samp::encrypt_for_group(&plaintext, &group.members, &nonce, &sender)
                .map_err(|e| SdkError::Encryption(e.to_string()))?;
        Ok(samp::encode_group(
            &nonce,
            &eph_pubkey,
            &capsules,
            &ciphertext,
        ))
    }

    pub async fn submit(&self, remark: &samp::RemarkBytes) -> Result<String> {
        let signing = self
            .signing
            .as_ref()
            .expect("signing key required for submit");
        crate::extrinsic::submit_remark(
            self.node_url.as_str(),
            remark,
            signing,
            &self.my_ss58,
            &self.chain_info,
        )
        .await
        .map_err(SdkError::from)
    }

    pub async fn fetch_balance(&self) -> Result<u128> {
        let pk = self.pubkey();
        Ok(crate::extrinsic::fetch_balance(
            self.node_url.as_str(),
            &pk,
            &self.chain_info.account_storage,
        )
        .await?)
    }

    pub async fn estimate_fee(&self, remark: &samp::RemarkBytes) -> Result<u128> {
        let signing = self
            .signing
            .as_ref()
            .expect("signing key required for fee estimation");
        Ok(crate::extrinsic::estimate_fee(
            self.node_url.as_str(),
            remark,
            signing,
            &self.my_ss58,
            &self.chain_info,
        )
        .await?)
    }

    pub fn cleanup_pending(&mut self) -> Option<CleanupResult> {
        let mut removed_thread = None;
        let mut removed_channel = None;
        let mut removed_group = None;

        if let Some(&idx) = self.thread_index.get(&BlockRef::ZERO) {
            self.threads.remove(idx);
            self.thread_index.remove(&BlockRef::ZERO);
            reindex(&mut self.thread_index, idx);
            removed_thread = Some(idx);
        }
        if let Some(&idx) = self.channel_index.get(&BlockRef::ZERO) {
            self.channels.remove(idx);
            self.channel_index.remove(&BlockRef::ZERO);
            reindex(&mut self.channel_index, idx);
            removed_channel = Some(idx);
        }
        if let Some(&idx) = self.group_index.get(&BlockRef::ZERO) {
            self.groups.remove(idx);
            self.group_index.remove(&BlockRef::ZERO);
            reindex(&mut self.group_index, idx);
            removed_group = Some(idx);
        }

        if removed_thread.is_some() || removed_channel.is_some() || removed_group.is_some() {
            Some(CleanupResult {
                removed_thread,
                removed_channel,
                removed_group,
            })
        } else {
            None
        }
    }
}

pub struct CleanupResult {
    pub removed_thread: Option<usize>,
    pub removed_channel: Option<usize>,
    pub removed_group: Option<usize>,
}
