//! Group membership, roles, notices, invitations, and profile synchronization.

#![allow(clippy::missing_errors_doc)]
#![allow(clippy::must_use_candidate)]
#![allow(clippy::needless_pass_by_value)]

use crate::agent_manager::EventRegistry;
use crate::network_manager::RawPacketReceivedEventArgs;
use crate::packet_catalog::{GeneratedPacket, PacketType};
use crate::{
    Error, GridClient, Group, GroupAccountSummary, GroupBanAction, GroupMember, GroupNotice,
    GroupNoticesListEntry, GroupProposal, GroupRole, GroupRoleChangeType, GroupRoleUpdate,
    GroupTitle, InstantMessageDialog,
};
use libremetaverse_structured_data::{OSD, OSDFormat, OSDParser};
use libremetaverse_types::compat::{CancellationToken, EventHandler, Subscription};
use libremetaverse_types::{AssetType, UUID, Vector3};
use std::collections::{HashMap, HashSet};
use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
use std::sync::{Arc, Mutex, RwLock};

const MAX_GROUPS: usize = 10_000;
const MAX_GROUP_ROWS: usize = 100_000;
const MAX_PENDING_REQUESTS: usize = 4_096;

fn mutex<T>(value: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
    value
        .lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn read<T>(value: &RwLock<T>) -> std::sync::RwLockReadGuard<'_, T> {
    value
        .read()
        .unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn write<T>(value: &RwLock<T>) -> std::sync::RwLockWriteGuard<'_, T> {
    value
        .write()
        .unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn wire_string(value: &[u8]) -> String {
    String::from_utf8_lossy(value.split(|b| *b == 0).next().unwrap_or_default()).into_owned()
}

macro_rules! event_args {
    ($name:ident { $($field:ident : $ty:ty => $getter:ident),+ $(,)? }) => {
        #[derive(Clone, Debug)]
        pub struct $name { $( $field: $ty ),+ }
        impl $name {
            pub fn new($($field: $ty),+) -> Result<Self, Error> { Ok(Self { $($field),+ }) }
            $(pub fn $getter(&self) -> $ty { self.$field.clone() })+
        }
    };
}

event_args!(BannedAgentsEventArgs { group_id: UUID => group_id, success: bool => success, banned_agents: Option<HashMap<UUID, std::time::SystemTime>> => banned_agents });
event_args!(CurrentGroupsEventArgs { groups: HashMap<UUID, Group> => groups });
event_args!(GroupAccountSummaryReplyEventArgs { group_id: UUID => group_id, summary: GroupAccountSummary => summary });
event_args!(GroupCreatedReplyEventArgs { group_id: UUID => group_id, success: bool => success, message: String => message });
event_args!(GroupDroppedEventArgs { group_id: UUID => group_id });
event_args!(GroupMembersReplyEventArgs { request_id: UUID => request_id, group_id: UUID => group_id, members: HashMap<UUID, GroupMember> => members });
event_args!(GroupNamesEventArgs { group_names: HashMap<UUID, String> => group_names });
event_args!(GroupNoticesListReplyEventArgs { group_id: UUID => group_id, notices: Vec<GroupNoticesListEntry> => notices });
event_args!(GroupOperationEventArgs { group_id: UUID => group_id, success: bool => success });
event_args!(GroupProfileEventArgs { group: Group => group });
event_args!(GroupRolesDataReplyEventArgs { request_id: UUID => request_id, group_id: UUID => group_id, roles: HashMap<UUID, GroupRole> => roles });
event_args!(GroupRolesMembersReplyEventArgs { request_id: UUID => request_id, group_id: UUID => group_id, roles_members: Vec<(UUID, UUID)> => roles_members });
event_args!(GroupTitlesReplyEventArgs { request_id: UUID => request_id, group_id: UUID => group_id, titles: HashMap<UUID, GroupTitle> => titles });

#[derive(Clone)]
pub struct GroupInvitationEventArgs {
    simulator: crate::Simulator,
    agent_id: UUID,
    from_name: String,
    message: String,
    accept: Arc<AtomicBool>,
}
impl GroupInvitationEventArgs {
    pub fn new(
        simulator: crate::Simulator,
        agent_id: UUID,
        agent_name: String,
        message: String,
    ) -> Result<Self, Error> {
        Ok(Self {
            simulator,
            agent_id,
            from_name: agent_name,
            message,
            accept: Arc::new(AtomicBool::new(false)),
        })
    }
    pub fn accept(&self) -> bool {
        self.accept.load(Ordering::Acquire)
    }
    pub fn set_accept(&mut self, value: bool) {
        self.accept.store(value, Ordering::Release);
    }
    pub fn agent_id(&self) -> UUID {
        self.agent_id
    }
    pub fn from_name(&self) -> String {
        self.from_name.clone()
    }
    pub fn message(&self) -> String {
        self.message.clone()
    }
    pub fn simulator(&self) -> crate::Simulator {
        self.simulator.clone()
    }
}

#[derive(Default)]
struct GroupEvents {
    banned: EventRegistry<BannedAgentsEventArgs>,
    current: EventRegistry<CurrentGroupsEventArgs>,
    account: EventRegistry<GroupAccountSummaryReplyEventArgs>,
    created: EventRegistry<GroupCreatedReplyEventArgs>,
    dropped: EventRegistry<GroupDroppedEventArgs>,
    invitation: EventRegistry<GroupInvitationEventArgs>,
    joined: EventRegistry<GroupOperationEventArgs>,
    left: EventRegistry<GroupOperationEventArgs>,
    ejected: EventRegistry<GroupOperationEventArgs>,
    members: EventRegistry<GroupMembersReplyEventArgs>,
    names: EventRegistry<GroupNamesEventArgs>,
    notices: EventRegistry<GroupNoticesListReplyEventArgs>,
    profile: EventRegistry<GroupProfileEventArgs>,
    roles: EventRegistry<GroupRolesDataReplyEventArgs>,
    role_members: EventRegistry<GroupRolesMembersReplyEventArgs>,
    titles: EventRegistry<GroupTitlesReplyEventArgs>,
}

#[derive(Default)]
struct PendingRows {
    members: HashMap<UUID, (UUID, usize, HashMap<UUID, GroupMember>)>,
    roles: HashMap<UUID, (UUID, usize, HashMap<UUID, GroupRole>)>,
    role_members: HashMap<UUID, PendingRoleMembers>,
    titles: HashMap<UUID, UUID>,
    accounts: HashMap<UUID, UUID>,
}
type PendingRoleMembers = (UUID, usize, Vec<(UUID, UUID)>, HashSet<(UUID, UUID)>);

pub(crate) struct GroupManagerInner {
    client: Arc<GridClient>,
    groups: RwLock<HashMap<UUID, Group>>,
    group_count: AtomicI32,
    pending: Mutex<PendingRows>,
    events: GroupEvents,
    subscriptions: Mutex<Vec<Subscription>>,
    disposed: AtomicBool,
}

#[derive(Clone)]
pub struct GroupManager {
    pub(crate) inner: Arc<GroupManagerInner>,
}

impl GroupManager {
    pub(crate) fn native_new(client: Arc<GridClient>) -> Self {
        if let Some(inner) = client.cached_group_manager_inner() {
            return Self { inner };
        }
        let inner = Arc::new(GroupManagerInner {
            client: Arc::clone(&client),
            groups: RwLock::new(HashMap::new()),
            group_count: AtomicI32::new(-1),
            pending: Mutex::new(PendingRows::default()),
            events: GroupEvents::default(),
            subscriptions: Mutex::new(Vec::new()),
            disposed: AtomicBool::new(false),
        });
        let weak = Arc::downgrade(&inner);
        mutex(&inner.subscriptions).push(client.network().subscribe_raw_packet(Arc::new(
            move |event| {
                if let Some(inner) = weak.upgrade() {
                    inner.handle_packet(event);
                }
            },
        )));
        let weak = Arc::downgrade(&inner);
        mutex(&inner.subscriptions).push(client.network().subscribe_caps(
            "AgentGroupDataUpdate".into(),
            crate::CapsEventQueueCallback::from_handler(move |_, message, _| {
                let Some(inner) = weak.upgrade() else {
                    return;
                };
                let Ok(map) = message.serialize() else {
                    return;
                };
                let Ok(mut decoded) = crate::messages::linden::AgentGroupDataUpdateMessage::new()
                else {
                    return;
                };
                if decoded.deserialize(map).is_ok() {
                    inner.handle_group_update_caps(decoded);
                }
            }),
        ));
        let weak = Arc::downgrade(&inner);
        mutex(&inner.subscriptions).push(client.network().subscribe_caps(
            "AgentDropGroup".into(),
            crate::CapsEventQueueCallback::from_handler(move |_, message, _| {
                let Some(inner) = weak.upgrade() else {
                    return;
                };
                let Ok(map) = message.serialize() else {
                    return;
                };
                let Ok(mut decoded) = crate::messages::linden::AgentDropGroupMessage::new() else {
                    return;
                };
                if decoded.deserialize(map).is_ok() {
                    for row in decoded.agent_data_block.into_iter().take(MAX_GROUPS) {
                        inner.drop_group(row.group_id);
                    }
                }
            }),
        ));
        let weak = Arc::downgrade(&inner);
        let mut callback_client = (*client).clone();
        mutex(&inner.subscriptions).push(callback_client.self_().subscribe_im(Arc::new(
            move |event| {
                if let Some(inner) = weak.upgrade() {
                    inner.handle_im(event);
                }
            },
        )));
        Self {
            inner: client.install_group_manager_inner(inner),
        }
    }
    pub fn new(client: GridClient) -> Result<Self, Error> {
        Ok(Self::native_new(Arc::new(client)))
    }
    fn ids(&self) -> (UUID, UUID) {
        let mut client = (*self.inner.client).clone();
        let agent = client.self_();
        (agent.agent_id(), agent.session_id())
    }
    fn send<T: GeneratedPacket>(&self, packet: &T, ty: PacketType) -> Result<(), Error> {
        if self.inner.disposed.load(Ordering::Acquire) {
            return Err(Error::InvalidOperation);
        }
        let sim = self
            .inner
            .client
            .network()
            .current_sim()
            .ok_or(Error::InvalidOperation)?;
        let bytes = packet.encode_packet()?;
        match sim.native_send_packet_data(
            bytes.clone(),
            i32::try_from(bytes.len()).map_err(|_| Error::Argument)?,
            ty,
            false,
        ) {
            Err(Error::InvalidOperation) if !sim.connected() => Ok(()),
            result => result,
        }
    }
    pub fn dispose(&self) -> Result<(), Error> {
        self.inner.dispose();
        Ok(())
    }
    pub fn current_group_count(&self) -> i32 {
        self.inner.group_count.load(Ordering::Acquire)
    }

    /// Returns a stable snapshot of the groups currently reported for this agent.
    ///
    /// The cache is replaced atomically whenever an `AgentGroupDataUpdate` arrives,
    /// so callers never observe a partially decoded group list.
    #[must_use]
    pub fn current_groups(&self) -> HashMap<UUID, Group> {
        read(&self.inner.groups).clone()
    }
    pub fn group_membership_limit(&self) -> i32 {
        let mut client = (*self.inner.client).clone();
        client.self_().benefits().group_membership_limit()
    }
    pub fn can_join_more_groups(&self) -> Option<Option<bool>> {
        let count = self.current_group_count();
        if count < 0 {
            Some(None)
        } else {
            Some(Some(count < self.group_membership_limit()))
        }
    }

    pub fn subscribe_banned_agents(&self, h: EventHandler<BannedAgentsEventArgs>) -> Subscription {
        self.inner.events.banned.subscribe(h)
    }
    pub fn subscribe_current_groups(
        &self,
        h: EventHandler<CurrentGroupsEventArgs>,
    ) -> Subscription {
        self.inner.events.current.subscribe(h)
    }
    pub fn subscribe_group_account_summary_reply(
        &self,
        h: EventHandler<GroupAccountSummaryReplyEventArgs>,
    ) -> Subscription {
        self.inner.events.account.subscribe(h)
    }
    pub fn subscribe_group_created_reply(
        &self,
        h: EventHandler<GroupCreatedReplyEventArgs>,
    ) -> Subscription {
        self.inner.events.created.subscribe(h)
    }
    pub fn subscribe_group_dropped(&self, h: EventHandler<GroupDroppedEventArgs>) -> Subscription {
        self.inner.events.dropped.subscribe(h)
    }
    pub fn subscribe_group_invitation(
        &self,
        h: EventHandler<GroupInvitationEventArgs>,
    ) -> Subscription {
        self.inner.events.invitation.subscribe(h)
    }
    pub fn subscribe_group_joined_reply(
        &self,
        h: EventHandler<GroupOperationEventArgs>,
    ) -> Subscription {
        self.inner.events.joined.subscribe(h)
    }
    pub fn subscribe_group_leave_reply(
        &self,
        h: EventHandler<GroupOperationEventArgs>,
    ) -> Subscription {
        self.inner.events.left.subscribe(h)
    }
    pub fn subscribe_group_member_ejected(
        &self,
        h: EventHandler<GroupOperationEventArgs>,
    ) -> Subscription {
        self.inner.events.ejected.subscribe(h)
    }
    pub fn subscribe_group_members_reply(
        &self,
        h: EventHandler<GroupMembersReplyEventArgs>,
    ) -> Subscription {
        self.inner.events.members.subscribe(h)
    }
    pub fn subscribe_group_names_reply(
        &self,
        h: EventHandler<GroupNamesEventArgs>,
    ) -> Subscription {
        self.inner.events.names.subscribe(h)
    }
    pub fn subscribe_group_notices_list_reply(
        &self,
        h: EventHandler<GroupNoticesListReplyEventArgs>,
    ) -> Subscription {
        self.inner.events.notices.subscribe(h)
    }
    pub fn subscribe_group_profile(&self, h: EventHandler<GroupProfileEventArgs>) -> Subscription {
        self.inner.events.profile.subscribe(h)
    }
    pub fn subscribe_group_role_data_reply(
        &self,
        h: EventHandler<GroupRolesDataReplyEventArgs>,
    ) -> Subscription {
        self.inner.events.roles.subscribe(h)
    }
    pub fn subscribe_group_role_members_reply(
        &self,
        h: EventHandler<GroupRolesMembersReplyEventArgs>,
    ) -> Subscription {
        self.inner.events.role_members.subscribe(h)
    }
    pub fn subscribe_group_titles_reply(
        &self,
        h: EventHandler<GroupTitlesReplyEventArgs>,
    ) -> Subscription {
        self.inner.events.titles.subscribe(h)
    }
}

impl GroupManagerInner {
    fn dispose(&self) {
        if !self.disposed.swap(true, Ordering::AcqRel) {
            mutex(&self.subscriptions).clear();
            mutex(&self.pending).members.clear();
            mutex(&self.pending).roles.clear();
            mutex(&self.pending).role_members.clear();
            mutex(&self.pending).titles.clear();
            mutex(&self.pending).accounts.clear();
        }
    }
    fn drop_group(&self, group_id: UUID) {
        let count = {
            let mut groups = write(&self.groups);
            groups.remove(&group_id);
            groups.len()
        };
        if self.group_count.load(Ordering::Acquire) >= 0 {
            self.group_count
                .store(i32::try_from(count).unwrap_or(i32::MAX), Ordering::Release);
        }
        self.events.dropped.emit(GroupDroppedEventArgs { group_id });
    }
    fn handle_group_update_caps(
        &self,
        message: crate::messages::linden::AgentGroupDataUpdateMessage,
    ) {
        let mut groups = HashMap::new();
        for (index, row) in message
            .group_data_block
            .into_iter()
            .take(MAX_GROUPS)
            .enumerate()
        {
            groups.insert(
                row.group_id,
                Group {
                    accept_notices: row.accept_notices,
                    allow_publish: false,
                    charter: String::new(),
                    contribution: row.contribution,
                    founder_id: UUID::zero(),
                    group_membership_count: 0,
                    group_roles_count: 0,
                    id: row.group_id,
                    insignia_id: row.group_insignia_id,
                    list_in_profile: message
                        .new_group_data_block
                        .get(index)
                        .is_some_and(|item| item.list_in_profile),
                    mature_publish: false,
                    member_title: row.group_title,
                    membership_fee: 0,
                    money: 0,
                    name: row.group_name,
                    open_enrollment: false,
                    owner_role: UUID::zero(),
                    powers: row.group_powers,
                    show_in_list: false,
                },
            );
        }
        write(&self.groups).clone_from(&groups);
        self.group_count.store(
            i32::try_from(groups.len()).unwrap_or(i32::MAX),
            Ordering::Release,
        );
        self.events.current.emit(CurrentGroupsEventArgs { groups });
    }
    fn handle_im(&self, event: crate::InstantMessageEventArgs) {
        let im = event.im();
        if im.dialog != InstantMessageDialog::GroupInvitation {
            return;
        }
        let Some(simulator) = event.simulator() else {
            return;
        };
        let args = GroupInvitationEventArgs {
            simulator,
            agent_id: im.from_agent_id,
            from_name: im.from_agent_name,
            message: im.message,
            accept: Arc::new(AtomicBool::new(false)),
        };
        self.events.invitation.emit(args.clone());
        let mut client = (*self.client).clone();
        let _ = client.self_().native_group_invite_respond(
            im.from_agent_id,
            im.im_session_id,
            args.accept(),
        );
    }
    #[allow(clippy::too_many_lines)]
    fn handle_packet(&self, event: RawPacketReceivedEventArgs) {
        if self.disposed.load(Ordering::Acquire) || event.data.len() > 8 * 1024 * 1024 {
            return;
        }
        let mut position = 0;
        match event.packet_type {
            PacketType::AgentGroupDataUpdate => {
                if let Ok(packet) = crate::packets::AgentGroupDataUpdatePacket::new_from_bytes(
                    &event.data,
                    &mut position,
                ) {
                    let groups = packet
                        .group_data
                        .into_iter()
                        .take(MAX_GROUPS)
                        .map(|row| {
                            let group = Group {
                                accept_notices: row.accept_notices,
                                allow_publish: false,
                                charter: String::new(),
                                contribution: row.contribution,
                                founder_id: UUID::zero(),
                                group_membership_count: 0,
                                group_roles_count: 0,
                                id: row.group_id,
                                insignia_id: row.group_insignia_id,
                                list_in_profile: false,
                                mature_publish: false,
                                member_title: String::new(),
                                membership_fee: 0,
                                money: 0,
                                name: wire_string(&row.group_name),
                                open_enrollment: false,
                                owner_role: UUID::zero(),
                                powers: crate::GroupPowers(row.group_powers),
                                show_in_list: false,
                            };
                            (group.id, group)
                        })
                        .collect::<HashMap<_, _>>();
                    write(&self.groups).clone_from(&groups);
                    self.group_count.store(
                        i32::try_from(groups.len()).unwrap_or(i32::MAX),
                        Ordering::Release,
                    );
                    self.events.current.emit(CurrentGroupsEventArgs { groups });
                }
            }
            PacketType::AgentDropGroup => {
                if let Ok(packet) =
                    crate::packets::AgentDropGroupPacket::new_from_bytes(&event.data, &mut position)
                {
                    self.drop_group(packet.agent_data.group_id);
                }
            }
            PacketType::GroupProfileReply => {
                if let Ok(packet) = crate::packets::GroupProfileReplyPacket::new_from_bytes(
                    &event.data,
                    &mut position,
                ) {
                    let row = packet.group_data;
                    let group = Group {
                        accept_notices: false,
                        allow_publish: row.allow_publish,
                        charter: wire_string(&row.charter),
                        contribution: 0,
                        founder_id: row.founder_id,
                        group_membership_count: row.group_membership_count,
                        group_roles_count: row.group_roles_count,
                        id: row.group_id,
                        insignia_id: row.insignia_id,
                        list_in_profile: false,
                        mature_publish: row.mature_publish,
                        member_title: wire_string(&row.member_title),
                        membership_fee: row.membership_fee,
                        money: row.money,
                        name: wire_string(&row.name),
                        open_enrollment: row.open_enrollment,
                        owner_role: row.owner_role,
                        powers: crate::GroupPowers(row.powers_mask),
                        show_in_list: row.show_in_list,
                    };
                    write(&self.groups).insert(group.id, group.clone());
                    self.events.profile.emit(GroupProfileEventArgs { group });
                }
            }
            PacketType::UUIDGroupNameReply => {
                if let Ok(packet) = crate::packets::UUIDGroupNameReplyPacket::new_from_bytes(
                    &event.data,
                    &mut position,
                ) {
                    let names = packet
                        .uuid_name_block
                        .into_iter()
                        .take(MAX_GROUPS)
                        .map(|row| (row.id, wire_string(&row.group_name)))
                        .collect();
                    self.events
                        .names
                        .emit(GroupNamesEventArgs { group_names: names });
                }
            }
            PacketType::GroupNoticesListReply => {
                if let Ok(packet) = crate::packets::GroupNoticesListReplyPacket::new_from_bytes(
                    &event.data,
                    &mut position,
                ) {
                    let notices = packet
                        .data
                        .into_iter()
                        .take(MAX_GROUP_ROWS)
                        .map(|row| GroupNoticesListEntry {
                            asset_type: asset_type(row.asset_type),
                            from_name: wire_string(&row.from_name),
                            has_attachment: row.has_attachment,
                            notice_id: row.notice_id,
                            subject: wire_string(&row.subject),
                            timestamp: row.timestamp,
                        })
                        .collect();
                    self.events.notices.emit(GroupNoticesListReplyEventArgs {
                        group_id: packet.agent_data.group_id,
                        notices,
                    });
                }
            }
            PacketType::GroupTitlesReply => {
                if let Ok(packet) = crate::packets::GroupTitlesReplyPacket::new_from_bytes(
                    &event.data,
                    &mut position,
                ) {
                    let group_id = packet.agent_data.group_id;
                    if mutex(&self.pending)
                        .titles
                        .remove(&packet.agent_data.request_id)
                        != Some(group_id)
                    {
                        return;
                    }
                    let titles = packet
                        .group_data
                        .into_iter()
                        .take(MAX_GROUP_ROWS)
                        .map(|row| {
                            (
                                row.role_id,
                                GroupTitle {
                                    group_id,
                                    role_id: row.role_id,
                                    selected: row.selected,
                                    title: wire_string(&row.title),
                                },
                            )
                        })
                        .collect();
                    self.events.titles.emit(GroupTitlesReplyEventArgs {
                        request_id: packet.agent_data.request_id,
                        group_id,
                        titles,
                    });
                }
            }
            PacketType::GroupMembersReply => self.handle_members(&event.data, &mut position),
            PacketType::GroupRoleDataReply => self.handle_roles(&event.data, &mut position),
            PacketType::GroupRoleMembersReply => {
                self.handle_role_members(&event.data, &mut position);
            }
            PacketType::GroupAccountSummaryReply => self.handle_account(&event.data, &mut position),
            PacketType::CreateGroupReply => {
                if let Ok(packet) = crate::packets::CreateGroupReplyPacket::new_from_bytes(
                    &event.data,
                    &mut position,
                ) {
                    self.events.created.emit(GroupCreatedReplyEventArgs {
                        group_id: packet.reply_data.group_id,
                        success: packet.reply_data.success,
                        message: wire_string(&packet.reply_data.message),
                    });
                }
            }
            PacketType::JoinGroupReply => {
                if let Ok(packet) =
                    crate::packets::JoinGroupReplyPacket::new_from_bytes(&event.data, &mut position)
                {
                    self.events.joined.emit(GroupOperationEventArgs {
                        group_id: packet.group_data.group_id,
                        success: packet.group_data.success,
                    });
                }
            }
            PacketType::LeaveGroupReply => {
                if let Ok(packet) = crate::packets::LeaveGroupReplyPacket::new_from_bytes(
                    &event.data,
                    &mut position,
                ) {
                    if packet.group_data.success {
                        let count = {
                            let mut groups = write(&self.groups);
                            groups.remove(&packet.group_data.group_id);
                            groups.len()
                        };
                        if self.group_count.load(Ordering::Acquire) >= 0 {
                            self.group_count
                                .store(i32::try_from(count).unwrap_or(i32::MAX), Ordering::Release);
                        }
                    }
                    self.events.left.emit(GroupOperationEventArgs {
                        group_id: packet.group_data.group_id,
                        success: packet.group_data.success,
                    });
                }
            }
            PacketType::EjectGroupMemberReply => {
                if let Ok(packet) = crate::packets::EjectGroupMemberReplyPacket::new_from_bytes(
                    &event.data,
                    &mut position,
                ) {
                    self.events.ejected.emit(GroupOperationEventArgs {
                        group_id: packet.group_data.group_id,
                        success: packet.eject_data.success,
                    });
                }
            }
            _ => {}
        }
    }
    fn handle_members(&self, data: &[u8], position: &mut i32) {
        let Ok(packet) = crate::packets::GroupMembersReplyPacket::new_from_bytes(data, position)
        else {
            return;
        };
        let Ok(expected) = usize::try_from(packet.group_data.member_count) else {
            return;
        };
        if expected > MAX_GROUP_ROWS {
            mutex(&self.pending)
                .members
                .remove(&packet.group_data.request_id);
            return;
        }
        let complete = {
            let mut pending = mutex(&self.pending);
            let Some((group_id, count, rows)) =
                pending.members.get_mut(&packet.group_data.request_id)
            else {
                return;
            };
            if *group_id != packet.group_data.group_id {
                return;
            }
            *count = expected;
            for row in packet.member_data.into_iter().take(MAX_GROUP_ROWS) {
                rows.entry(row.agent_id).or_insert_with(|| GroupMember {
                    contribution: row.contribution,
                    id: row.agent_id,
                    is_owner: row.is_owner,
                    online_status: wire_string(&row.online_status),
                    powers: crate::GroupPowers(row.agent_powers),
                    title: wire_string(&row.title),
                });
            }
            if rows.len() >= *count {
                pending
                    .members
                    .remove(&packet.group_data.request_id)
                    .map(|(_, _, rows)| rows)
            } else {
                None
            }
        };
        if let Some(members) = complete {
            self.events.members.emit(GroupMembersReplyEventArgs {
                request_id: packet.group_data.request_id,
                group_id: packet.group_data.group_id,
                members,
            });
        }
    }
    fn handle_roles(&self, data: &[u8], position: &mut i32) {
        let Ok(packet) = crate::packets::GroupRoleDataReplyPacket::new_from_bytes(data, position)
        else {
            return;
        };
        let Ok(expected) = usize::try_from(packet.group_data.role_count) else {
            return;
        };
        if expected > MAX_GROUP_ROWS {
            mutex(&self.pending)
                .roles
                .remove(&packet.group_data.request_id);
            return;
        }
        let complete = {
            let mut pending = mutex(&self.pending);
            let Some((group_id, count, rows)) =
                pending.roles.get_mut(&packet.group_data.request_id)
            else {
                return;
            };
            if *group_id != packet.group_data.group_id {
                return;
            }
            *count = expected;
            for row in packet.role_data.into_iter().take(MAX_GROUP_ROWS) {
                rows.entry(row.role_id).or_insert_with(|| GroupRole {
                    description: wire_string(&row.description),
                    group_id: packet.group_data.group_id,
                    id: row.role_id,
                    members: row.members,
                    name: wire_string(&row.name),
                    powers: crate::GroupPowers(row.powers),
                    title: wire_string(&row.title),
                });
            }
            if rows.len() >= *count {
                pending
                    .roles
                    .remove(&packet.group_data.request_id)
                    .map(|(_, _, rows)| rows)
            } else {
                None
            }
        };
        if let Some(roles) = complete {
            self.events.roles.emit(GroupRolesDataReplyEventArgs {
                request_id: packet.group_data.request_id,
                group_id: packet.group_data.group_id,
                roles,
            });
        }
    }
    fn handle_role_members(&self, data: &[u8], position: &mut i32) {
        let Ok(packet) =
            crate::packets::GroupRoleMembersReplyPacket::new_from_bytes(data, position)
        else {
            return;
        };
        let Ok(expected) = usize::try_from(packet.agent_data.total_pairs) else {
            return;
        };
        if expected > MAX_GROUP_ROWS {
            mutex(&self.pending)
                .role_members
                .remove(&packet.agent_data.request_id);
            return;
        }
        let complete = {
            let mut pending = mutex(&self.pending);
            let Some((group_id, count, rows, seen)) =
                pending.role_members.get_mut(&packet.agent_data.request_id)
            else {
                return;
            };
            if *group_id != packet.agent_data.group_id {
                return;
            }
            *count = expected;
            for row in packet.member_data.into_iter().take(MAX_GROUP_ROWS) {
                let pair = (row.role_id, row.member_id);
                if seen.insert(pair) {
                    rows.push(pair);
                }
            }
            if rows.len() >= *count {
                pending
                    .role_members
                    .remove(&packet.agent_data.request_id)
                    .map(|(_, _, rows, _)| rows)
            } else {
                None
            }
        };
        if let Some(roles_members) = complete {
            self.events
                .role_members
                .emit(GroupRolesMembersReplyEventArgs {
                    request_id: packet.agent_data.request_id,
                    group_id: packet.agent_data.group_id,
                    roles_members,
                });
        }
    }
    fn handle_account(&self, data: &[u8], position: &mut i32) {
        let Ok(packet) =
            crate::packets::GroupAccountSummaryReplyPacket::new_from_bytes(data, position)
        else {
            return;
        };
        let row = packet.money_data;
        if mutex(&self.pending).accounts.remove(&row.request_id) != Some(packet.agent_data.group_id)
        {
            return;
        }
        let summary = GroupAccountSummary {
            balance: row.balance,
            current_interval: row.current_interval,
            group_tax_current: row.group_tax_current,
            group_tax_estimate: row.group_tax_estimate,
            interval_days: row.interval_days,
            land_tax_current: row.land_tax_current,
            land_tax_estimate: row.land_tax_estimate,
            last_tax_date: wire_string(&row.last_tax_date),
            light_tax_current: row.light_tax_current,
            light_tax_estimate: row.light_tax_estimate,
            non_exempt_members: row.non_exempt_members,
            object_tax_current: row.object_tax_current,
            object_tax_estimate: row.object_tax_estimate,
            parcel_dir_fee_current: row.parcel_dir_fee_current,
            parcel_dir_fee_estimate: row.parcel_dir_fee_estimate,
            start_date: wire_string(&row.start_date),
            tax_date: wire_string(&row.tax_date),
            total_credits: row.total_credits,
            total_debits: row.total_debits,
        };
        self.events.account.emit(GroupAccountSummaryReplyEventArgs {
            group_id: packet.agent_data.group_id,
            summary,
        });
    }
}

fn asset_type(value: u8) -> AssetType {
    match value.cast_signed() {
        0 => AssetType::Texture,
        1 => AssetType::Sound,
        2 => AssetType::CallingCard,
        3 => AssetType::Landmark,
        4 => AssetType::Script,
        5 => AssetType::Clothing,
        6 => AssetType::Object,
        7 => AssetType::Notecard,
        8 => AssetType::Folder,
        10 => AssetType::LSLText,
        11 => AssetType::LSLBytecode,
        12 => AssetType::TextureTGA,
        13 => AssetType::Bodypart,
        17 => AssetType::SoundWAV,
        18 => AssetType::ImageTGA,
        19 => AssetType::ImageJPEG,
        20 => AssetType::Animation,
        21 => AssetType::Gesture,
        22 => AssetType::Simstate,
        24 => AssetType::Link,
        25 => AssetType::LinkFolder,
        40 => AssetType::Widget,
        45 => AssetType::Person,
        49 => AssetType::Mesh,
        56 => AssetType::Settings,
        57 => AssetType::Material,
        _ => AssetType::Unknown,
    }
}

impl GroupManager {
    pub fn request_current_groups(&self) -> Result<(), Error> {
        let (agent, session) = self.ids();
        let mut packet = crate::packets::AgentDataUpdateRequestPacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent;
        packet.agent_data.session_id = session;
        self.send(&packet, PacketType::AgentDataUpdateRequest)
    }
    pub fn request_group_name(&self, group_id: UUID) -> Result<(), Error> {
        self.request_group_names(vec![group_id])
    }
    pub fn request_group_names(&self, group_i_ds: Vec<UUID>) -> Result<(), Error> {
        if group_i_ds.is_empty() {
            return Ok(());
        }
        if group_i_ds.len() > MAX_GROUPS {
            return Err(Error::Argument);
        }
        let mut packet = crate::packets::UUIDGroupNameRequestPacket::new_with_constructor()?;
        packet.uuid_name_block = group_i_ds
            .into_iter()
            .map(|id| crate::packets::UUIDGroupNameRequestPacketUUIDNameBlockBlock { id })
            .collect();
        self.send(&packet, PacketType::UUIDGroupNameRequest)
    }
    pub fn request_group_profile(&self, group: UUID) -> Result<(), Error> {
        let (agent, session) = self.ids();
        let mut packet = crate::packets::GroupProfileRequestPacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent;
        packet.agent_data.session_id = session;
        packet.group_data.group_id = group;
        self.send(&packet, PacketType::GroupProfileRequest)
    }
    pub fn request_group_members(&self, group: UUID) -> Result<UUID, Error> {
        let request_id = UUID::random()?;
        let (agent, session) = self.ids();
        {
            let mut pending = mutex(&self.inner.pending);
            if pending.members.len() >= MAX_PENDING_REQUESTS {
                return Err(Error::InvalidOperation);
            }
            pending
                .members
                .insert(request_id, (group, usize::MAX, HashMap::new()));
        }
        let mut packet = crate::packets::GroupMembersRequestPacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent;
        packet.agent_data.session_id = session;
        packet.group_data.group_id = group;
        packet.group_data.request_id = request_id;
        if let Err(error) = self.send(&packet, PacketType::GroupMembersRequest) {
            mutex(&self.inner.pending).members.remove(&request_id);
            return Err(error);
        }
        Ok(request_id)
    }
    pub fn request_group_roles(&self, group: UUID) -> Result<UUID, Error> {
        let request_id = UUID::random()?;
        let (agent, session) = self.ids();
        {
            let mut pending = mutex(&self.inner.pending);
            if pending.roles.len() >= MAX_PENDING_REQUESTS {
                return Err(Error::InvalidOperation);
            }
            pending
                .roles
                .insert(request_id, (group, usize::MAX, HashMap::new()));
        }
        let mut packet = crate::packets::GroupRoleDataRequestPacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent;
        packet.agent_data.session_id = session;
        packet.group_data.group_id = group;
        packet.group_data.request_id = request_id;
        if let Err(error) = self.send(&packet, PacketType::GroupRoleDataRequest) {
            mutex(&self.inner.pending).roles.remove(&request_id);
            return Err(error);
        }
        Ok(request_id)
    }
    pub fn request_group_roles_members(&self, group: UUID) -> Result<UUID, Error> {
        let request_id = UUID::random()?;
        let (agent, session) = self.ids();
        {
            let mut pending = mutex(&self.inner.pending);
            if pending.role_members.len() >= MAX_PENDING_REQUESTS {
                return Err(Error::InvalidOperation);
            }
            pending
                .role_members
                .insert(request_id, (group, usize::MAX, Vec::new(), HashSet::new()));
        }
        let mut packet = crate::packets::GroupRoleMembersRequestPacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent;
        packet.agent_data.session_id = session;
        packet.group_data.group_id = group;
        packet.group_data.request_id = request_id;
        if let Err(error) = self.send(&packet, PacketType::GroupRoleMembersRequest) {
            mutex(&self.inner.pending).role_members.remove(&request_id);
            return Err(error);
        }
        Ok(request_id)
    }
    pub fn request_group_titles(&self, group: UUID) -> Result<UUID, Error> {
        let request_id = UUID::random()?;
        let (agent, session) = self.ids();
        {
            let mut pending = mutex(&self.inner.pending);
            if pending.titles.len() >= MAX_PENDING_REQUESTS {
                return Err(Error::InvalidOperation);
            }
            pending.titles.insert(request_id, group);
        }
        let mut packet = crate::packets::GroupTitlesRequestPacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent;
        packet.agent_data.session_id = session;
        packet.agent_data.group_id = group;
        packet.agent_data.request_id = request_id;
        if let Err(error) = self.send(&packet, PacketType::GroupTitlesRequest) {
            mutex(&self.inner.pending).titles.remove(&request_id);
            return Err(error);
        }
        Ok(request_id)
    }
    pub fn request_group_account_summary(
        &self,
        group: UUID,
        interval_days: i32,
        current_interval: i32,
    ) -> Result<(), Error> {
        let (agent, session) = self.ids();
        let mut packet = crate::packets::GroupAccountSummaryRequestPacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent;
        packet.agent_data.session_id = session;
        packet.agent_data.group_id = group;
        let request_id = UUID::random()?;
        {
            let mut pending = mutex(&self.inner.pending);
            if pending.accounts.len() >= MAX_PENDING_REQUESTS {
                return Err(Error::InvalidOperation);
            }
            pending.accounts.insert(request_id, group);
        }
        packet.money_data.request_id = request_id;
        packet.money_data.current_interval = current_interval;
        packet.money_data.interval_days = interval_days;
        if let Err(error) = self.send(&packet, PacketType::GroupAccountSummaryRequest) {
            mutex(&self.inner.pending).accounts.remove(&request_id);
            Err(error)
        } else {
            Ok(())
        }
    }
    pub fn request_group_notices_list(&self, group: UUID) -> Result<(), Error> {
        let (agent, session) = self.ids();
        let mut packet = crate::packets::GroupNoticesListRequestPacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent;
        packet.agent_data.session_id = session;
        packet.data.group_id = group;
        self.send(&packet, PacketType::GroupNoticesListRequest)
    }
    pub fn request_group_notice(&self, notice_id: UUID) -> Result<(), Error> {
        let (agent, session) = self.ids();
        let mut packet = crate::packets::GroupNoticeRequestPacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent;
        packet.agent_data.session_id = session;
        packet.data.group_notice_id = notice_id;
        self.send(&packet, PacketType::GroupNoticeRequest)
    }
    pub fn activate_group(&self, id: UUID) -> Result<(), Error> {
        let (agent, session) = self.ids();
        let mut packet = crate::packets::ActivateGroupPacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent;
        packet.agent_data.session_id = session;
        packet.agent_data.group_id = id;
        self.send(&packet, PacketType::ActivateGroup)
    }
    pub fn activate_title(&self, group: UUID, role: UUID) -> Result<(), Error> {
        let (agent, session) = self.ids();
        let mut packet = crate::packets::GroupTitleUpdatePacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent;
        packet.agent_data.session_id = session;
        packet.agent_data.group_id = group;
        packet.agent_data.title_role_id = role;
        self.send(&packet, PacketType::GroupTitleUpdate)
    }
    pub fn set_group_contribution(&self, group: UUID, contribution: i32) -> Result<(), Error> {
        if contribution < 0 {
            return Err(Error::Argument);
        }
        let (agent, session) = self.ids();
        let mut packet = crate::packets::SetGroupContributionPacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent;
        packet.agent_data.session_id = session;
        packet.data.group_id = group;
        packet.data.contribution = contribution;
        self.send(&packet, PacketType::SetGroupContribution)
    }
    pub fn set_group_accept_notices(
        &self,
        group_id: UUID,
        accept_notices: bool,
        list_in_profile: bool,
    ) -> Result<(), Error> {
        let (agent, session) = self.ids();
        let mut packet = crate::packets::SetGroupAcceptNoticesPacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent;
        packet.agent_data.session_id = session;
        packet.data.group_id = group_id;
        packet.data.accept_notices = accept_notices;
        packet.new_data.list_in_profile = list_in_profile;
        self.send(&packet, PacketType::SetGroupAcceptNotices)
    }
    pub fn request_join_group(&self, id: UUID) -> Result<(), Error> {
        let (agent, session) = self.ids();
        let mut packet = crate::packets::JoinGroupRequestPacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent;
        packet.agent_data.session_id = session;
        packet.group_data.group_id = id;
        self.send(&packet, PacketType::JoinGroupRequest)
    }
    pub fn leave_group(&self, group_id: UUID) -> Result<(), Error> {
        let (agent, session) = self.ids();
        let mut packet = crate::packets::LeaveGroupRequestPacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent;
        packet.agent_data.session_id = session;
        packet.group_data.group_id = group_id;
        self.send(&packet, PacketType::LeaveGroupRequest)
    }

    pub fn invite(
        &self,
        group: UUID,
        roles: Vec<UUID>,
        requested_agent: UUID,
    ) -> Result<(), Error> {
        if roles.is_empty() || roles.len() > 256 {
            return Err(Error::Argument);
        }
        let (agent, session) = self.ids();
        let mut packet = crate::packets::InviteGroupRequestPacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent;
        packet.agent_data.session_id = session;
        packet.group_data.group_id = group;
        packet.invite_data = roles
            .into_iter()
            .map(
                |role_id| crate::packets::InviteGroupRequestPacketInviteDataBlock {
                    invitee_id: requested_agent,
                    role_id,
                },
            )
            .collect();
        self.send(&packet, PacketType::InviteGroupRequest)
    }
    pub fn request_create_group(&self, group: Group) -> Result<(), Error> {
        validate_group(&group)?;
        let (agent, session) = self.ids();
        let mut packet = crate::packets::CreateGroupRequestPacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent;
        packet.agent_data.session_id = session;
        packet.group_data.allow_publish = group.allow_publish;
        packet.group_data.charter = group.charter.into_bytes();
        packet.group_data.insignia_id = group.insignia_id;
        packet.group_data.mature_publish = group.mature_publish;
        packet.group_data.membership_fee = group.membership_fee;
        packet.group_data.name = group.name.into_bytes();
        packet.group_data.open_enrollment = group.open_enrollment;
        packet.group_data.show_in_list = group.show_in_list;
        self.send(&packet, PacketType::CreateGroupRequest)
    }
    pub fn update_group(&self, id: UUID, group: Group) -> Result<(), Error> {
        validate_group(&group)?;
        let (agent, session) = self.ids();
        let mut packet = crate::packets::UpdateGroupInfoPacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent;
        packet.agent_data.session_id = session;
        packet.group_data.group_id = id;
        packet.group_data.allow_publish = group.allow_publish;
        packet.group_data.charter = group.charter.into_bytes();
        packet.group_data.insignia_id = group.insignia_id;
        packet.group_data.mature_publish = group.mature_publish;
        packet.group_data.membership_fee = group.membership_fee;
        packet.group_data.open_enrollment = group.open_enrollment;
        packet.group_data.show_in_list = group.show_in_list;
        self.send(&packet, PacketType::UpdateGroupInfo)
    }
    pub fn eject_user(&self, group: UUID, member: UUID) -> Result<(), Error> {
        let (agent, session) = self.ids();
        let mut packet = crate::packets::EjectGroupMemberRequestPacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent;
        packet.agent_data.session_id = session;
        packet.group_data.group_id = group;
        let block =
            crate::packets::EjectGroupMemberRequestPacketEjectDataBlock { ejectee_id: member };
        packet.eject_data = vec![block];
        self.send(&packet, PacketType::EjectGroupMemberRequest)
    }
    pub fn update_role(&self, role: GroupRole) -> Result<(), Error> {
        self.send_role_update(role.group_id, role, GroupRoleUpdate::UpdateAll)
    }
    pub fn create_role(&self, group: UUID, mut role: GroupRole) -> Result<(), Error> {
        role.id = UUID::random()?;
        self.send_role_update(group, role, GroupRoleUpdate::Create)
    }
    pub fn delete_role(&self, group: UUID, role_id: UUID) -> Result<(), Error> {
        let role = GroupRole {
            description: String::new(),
            group_id: group,
            id: role_id,
            members: 0,
            name: String::new(),
            powers: crate::GroupPowers::NONE,
            title: String::new(),
        };
        self.send_role_update(group, role, GroupRoleUpdate::Delete)
    }
    fn send_role_update(
        &self,
        group: UUID,
        role: GroupRole,
        update: GroupRoleUpdate,
    ) -> Result<(), Error> {
        if role.name.len() > 256 || role.title.len() > 256 || role.description.len() > 1024 {
            return Err(Error::Argument);
        }
        let (agent, session) = self.ids();
        let mut packet = crate::packets::GroupRoleUpdatePacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent;
        packet.agent_data.session_id = session;
        packet.agent_data.group_id = group;
        let block = crate::packets::GroupRoleUpdatePacketRoleDataBlock {
            role_id: role.id,
            name: role.name.into_bytes(),
            description: role.description.into_bytes(),
            powers: role.powers.0,
            title: role.title.into_bytes(),
            update_type: update as u8,
        };
        packet.role_data = vec![block];
        self.send(&packet, PacketType::GroupRoleUpdate)
    }
    pub fn add_to_role(&self, group: UUID, role: UUID, member: UUID) -> Result<(), Error> {
        self.change_role(group, role, member, GroupRoleChangeType::AddToRole)
    }
    pub fn remove_from_role(&self, group: UUID, role: UUID, member: UUID) -> Result<(), Error> {
        self.change_role(group, role, member, GroupRoleChangeType::RemoveFromRole)
    }
    fn change_role(
        &self,
        group: UUID,
        role: UUID,
        member: UUID,
        change: GroupRoleChangeType,
    ) -> Result<(), Error> {
        let (agent, session) = self.ids();
        let mut packet = crate::packets::GroupRoleChangesPacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent;
        packet.agent_data.session_id = session;
        packet.agent_data.group_id = group;
        let block = crate::packets::GroupRoleChangesPacketRoleChangeBlock {
            member_id: member,
            role_id: role,
            change: change as u32,
        };
        packet.role_change = vec![block];
        self.send(&packet, PacketType::GroupRoleChanges)
    }
    pub fn start_proposal(&self, group: UUID, prop: GroupProposal) -> Result<(), Error> {
        if prop.vote_text.len() > 1024
            || prop.duration <= 0
            || prop.quorum < 0
            || !(0.0..=1.0).contains(&prop.majority)
        {
            return Err(Error::Argument);
        }
        let (agent, session) = self.ids();
        let mut packet = crate::packets::StartGroupProposalPacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent;
        packet.agent_data.session_id = session;
        packet.proposal_data.group_id = group;
        packet.proposal_data.proposal_text = prop.vote_text.into_bytes();
        packet.proposal_data.quorum = prop.quorum;
        packet.proposal_data.majority = prop.majority;
        packet.proposal_data.duration = prop.duration;
        self.send(&packet, PacketType::StartGroupProposal)
    }
    pub fn send_group_notice(&self, group: UUID, notice: GroupNotice) -> Result<(), Error> {
        if notice.subject.len() > 256 || notice.message.len() > 4096 {
            return Err(Error::Argument);
        }
        let bucket = serialize_notice_attachment(&notice)?;
        let mut client = (*self.inner.client).clone();
        let region = client
            .network()
            .current_sim()
            .map_or_else(UUID::zero, |sim| sim.id);
        let agent = client.self_();
        agent.native_instant_message_full(
            agent.name(),
            group,
            format!("{}|{}", notice.subject, notice.message),
            UUID::zero(),
            InstantMessageDialog::GroupNotice,
            crate::InstantMessageOnline::Online,
            Vector3::zero(),
            region,
            bucket,
        )
    }

    pub async fn request_banned_agents(
        &self,
        group_id: UUID,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<(), Error> {
        let token = cancellation_token.unwrap_or_else(|| self.inner.client.cancellation_token());
        let result = self.group_api_uri(group_id).map(|uri| (uri, token.clone()));
        let Ok((uri, token)) = result else {
            self.inner.events.banned.emit(BannedAgentsEventArgs {
                group_id,
                success: false,
                banned_agents: None,
            });
            return result.map(|_| ());
        };
        token.throw_if_cancellation_requested()?;
        let response = self
            .inner
            .client
            .native_http_caps_client()
            .get(uri, token, None)
            .await;
        let parsed = response.and_then(|(response, bytes)| {
            if !response.is_success_status_code() || bytes.len() > 4 * 1024 * 1024 {
                return Err(Error::InvalidOperation);
            }
            let OSD::Map(root) = OSDParser::deserialize_with_bytes(bytes)? else {
                return Err(Error::Argument);
            };
            let mut agents = HashMap::new();
            if let Some(OSD::Map(list)) = root.get("ban_list") {
                if list.len() > MAX_GROUP_ROWS {
                    return Err(Error::Argument);
                }
                for (id, value) in list {
                    let Ok(id) = UUID::new_with_string(id.clone()) else {
                        continue;
                    };
                    let OSD::Map(fields) = value else { continue };
                    if let Some(date) = fields.get("ban_date").and_then(|date| date.as_date().ok())
                    {
                        agents.insert(id, date);
                    }
                }
            }
            Ok(agents)
        });
        match parsed {
            Ok(agents) => {
                self.inner.events.banned.emit(BannedAgentsEventArgs {
                    group_id,
                    success: true,
                    banned_agents: Some(agents),
                });
                Ok(())
            }
            Err(error) => {
                self.inner.events.banned.emit(BannedAgentsEventArgs {
                    group_id,
                    success: false,
                    banned_agents: None,
                });
                Err(error)
            }
        }
    }
    pub async fn request_ban_action(
        &self,
        group_id: UUID,
        action: GroupBanAction,
        agents: Vec<UUID>,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<(), Error> {
        if agents.is_empty() || agents.len() > MAX_GROUP_ROWS {
            return Err(Error::Argument);
        }
        let token = cancellation_token.unwrap_or_else(|| self.inner.client.cancellation_token());
        token.throw_if_cancellation_requested()?;
        let payload = OSD::Map(HashMap::from([
            ("ban_action".into(), OSD::Integer(action as i32)),
            (
                "ban_ids".into(),
                OSD::Array(agents.into_iter().map(OSD::UUID).collect()),
            ),
        ]));
        let (response, bytes) = self
            .inner
            .client
            .native_http_caps_client()
            .post_with_uri_osd_format_osd_cancellation_token_i_progress(
                self.group_api_uri(group_id)?,
                OSDFormat::Xml,
                payload,
                token,
                None,
            )
            .await?;
        if !response.is_success_status_code() || bytes.len() > 1024 * 1024 {
            Err(Error::InvalidOperation)
        } else {
            Ok(())
        }
    }
    fn group_api_uri(&self, group_id: UUID) -> Result<libremetaverse_types::compat::Uri, Error> {
        let caps = self
            .inner
            .client
            .network()
            .current_sim()
            .and_then(|sim| sim.native_caps())
            .ok_or(Error::InvalidOperation)?;
        let uri = caps
            .capability_uri("GroupAPIv1".into())?
            .ok_or(Error::InvalidOperation)?;
        let mut url = reqwest::Url::parse(&uri.0).map_err(|_| Error::Argument)?;
        url.query_pairs_mut()
            .append_pair("group_id", &group_id.to_string());
        Ok(libremetaverse_types::compat::Uri(url.into()))
    }
}

fn validate_group(group: &Group) -> Result<(), Error> {
    if group.name.is_empty()
        || group.name.len() > 256
        || group.charter.len() > 4096
        || group.membership_fee < 0
    {
        Err(Error::Argument)
    } else {
        Ok(())
    }
}

pub(crate) fn serialize_notice_attachment(notice: &GroupNotice) -> Result<Vec<u8>, Error> {
    if notice.owner_id == UUID::zero() || notice.attachment_id == UUID::zero() {
        return Ok(Vec::new());
    }
    OSDParser::serialize_llsd_xml_bytes(OSD::Map(HashMap::from([
        ("item_id".into(), OSD::UUID(notice.attachment_id)),
        ("owner_id".into(), OSD::UUID(notice.owner_id)),
    ])))
}

impl Group {
    pub(crate) fn native_equals_group(&self, other: &Self) -> bool {
        self == other
    }
    pub(crate) fn native_equals_object(
        &self,
        other: Option<libremetaverse_types::compat::Object>,
    ) -> bool {
        other
            .as_ref()
            .and_then(libremetaverse_types::compat::Object::downcast_ref::<Self>)
            == Some(self)
    }
    pub(crate) fn native_hash_code(&self) -> i32 {
        let Ok(bytes) = self.id.get_bytes() else {
            return 0;
        };
        i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
    }
    pub(crate) fn native_to_string(&self) -> String {
        format!("{} (Members: {})", self.name, self.group_membership_count)
    }
}
impl GroupNotice {
    pub(crate) fn native_serialize_attachment(&self) -> Result<Vec<u8>, Error> {
        serialize_notice_attachment(self)
    }
}
impl GroupRole {
    pub(crate) fn native_to_string(&self) -> String {
        format!("{} ({})", self.name, self.title)
    }
}
impl GroupTitle {
    pub(crate) fn native_to_string(&self) -> String {
        self.title.clone()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::net::{IpAddr, Ipv4Addr, SocketAddr};

    fn id(value: &str) -> UUID {
        UUID::new_with_string(value.into()).unwrap()
    }

    fn simulator(client: &GridClient) -> crate::Simulator {
        crate::Simulator::native_new(
            client.clone(),
            SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 13972),
            0,
            Some(256),
            Some(256),
        )
        .unwrap()
    }

    fn member_packet(group_id: UUID, request_id: UUID, member_id: UUID) -> Vec<u8> {
        let mut packet = crate::packets::GroupMembersReplyPacket::new_with_constructor().unwrap();
        packet.group_data.group_id = group_id;
        packet.group_data.request_id = request_id;
        packet.group_data.member_count = 2;
        let mut member =
            crate::packets::GroupMembersReplyPacketMemberDataBlock::new_with_constructor().unwrap();
        member.agent_id = member_id;
        member.title = b"Builder".to_vec();
        member.online_status = b"Online".to_vec();
        member.agent_powers = crate::GroupPowers::MEMBER_VISIBLE.0;
        packet.member_data = vec![member];
        packet.encode_packet().unwrap()
    }

    #[test]
    fn fragmented_members_are_correlated_deduplicated_and_emitted_after_unlock() {
        let client = GridClient::new().unwrap();
        let manager = GroupManager::native_new(Arc::new(client.clone()));
        let simulator = simulator(&client);
        let group_id = id("11111111-1111-1111-1111-111111111111");
        let request_id = id("22222222-2222-2222-2222-222222222222");
        let first = id("33333333-3333-3333-3333-333333333333");
        let second = id("44444444-4444-4444-4444-444444444444");
        mutex(&manager.inner.pending)
            .members
            .insert(request_id, (group_id, usize::MAX, HashMap::new()));
        let received = Arc::new(Mutex::new(Vec::new()));
        let received_copy = Arc::clone(&received);
        let inner = Arc::clone(&manager.inner);
        let _subscription = manager.subscribe_group_members_reply(Arc::new(move |event| {
            assert!(
                !mutex(&inner.pending)
                    .members
                    .contains_key(&event.request_id())
            );
            received_copy.lock().unwrap().push(event.members());
        }));
        for member in [first, first, second] {
            manager.inner.handle_packet(RawPacketReceivedEventArgs {
                packet_type: PacketType::GroupMembersReply,
                data: member_packet(group_id, request_id, member),
                simulator: simulator.clone(),
            });
        }
        let values = received.lock().unwrap();
        assert_eq!(values.len(), 1);
        assert_eq!(values[0].len(), 2);
        assert_eq!(values[0][&first].title, "Builder");
    }

    #[test]
    fn notice_attachment_is_structured_and_empty_without_both_ids() {
        let empty = GroupNotice {
            attachment_id: UUID::zero(),
            message: String::new(),
            owner_id: UUID::zero(),
            subject: String::new(),
        };
        assert!(serialize_notice_attachment(&empty).unwrap().is_empty());
        let notice = GroupNotice {
            attachment_id: id("55555555-5555-5555-5555-555555555555"),
            owner_id: id("66666666-6666-6666-6666-666666666666"),
            ..empty
        };
        let OSD::Map(map) =
            OSDParser::deserialize_with_bytes(serialize_notice_attachment(&notice).unwrap())
                .unwrap()
        else {
            panic!("attachment must be an LLSD map")
        };
        assert_eq!(map["item_id"].as_uuid().unwrap(), notice.attachment_id);
        assert_eq!(map["owner_id"].as_uuid().unwrap(), notice.owner_id);
    }

    #[test]
    fn dispose_releases_callbacks_and_pending_requests() {
        let client = GridClient::new().unwrap();
        let manager = GroupManager::native_new(Arc::new(client));
        mutex(&manager.inner.pending)
            .members
            .insert(UUID::zero(), (UUID::zero(), 1, HashMap::new()));
        manager.dispose().unwrap();
        assert!(manager.inner.disposed.load(Ordering::Acquire));
        assert!(mutex(&manager.inner.pending).members.is_empty());
        assert!(mutex(&manager.inner.subscriptions).is_empty());
    }
}
