//! Native non-movement agent services and event delivery.

#![allow(clippy::inherent_to_string)] // Public names mirror C# ToString mappings.
#![allow(clippy::cast_possible_truncation)] // C# double-to-float mappings truncate identically.
#![allow(clippy::missing_errors_doc)] // Result shapes are fixed by the compatibility API.
#![allow(clippy::needless_pass_by_value)] // Owned parameters mirror mapped C# signatures.
#![allow(clippy::too_many_arguments)] // Packet-oriented signatures mirror the reference API.
#![allow(clippy::unnecessary_wraps)] // Generated callers require mapped Result signatures.
#![allow(clippy::unused_self)] // Instance shape is part of the mapped callback surface.
#![allow(clippy::if_not_else)] // Protocol mismatch handling is clearest as the early branch.

use crate::messages::linden::AgentStateUpdateMessage;
use crate::messages::linden::{
    AgentPreferencesMessage, AvatarRenderInfoMessage, ExperienceInfo, ExperienceInfoMessage,
    ExperienceListMessage, ExperiencePreferencesMessage, RegionExperiencesMessage,
    ViewerBenefitsMessage, ViewerStatsMessage,
};
use crate::{
    AgentAccessEventArgs, AgentDataReplyEventArgs, AgentExperiencesEventArgs,
    AgentPreferencesEventArgs, AlertMessageEventArgs, AnimationsChangedEventArgs,
    AvatarRenderInfoEventArgs, BalanceEventArgs, ChatEventArgs, ChatSessionMemberAddedEventArgs,
    ChatSessionMemberLeftEventArgs, ExperienceInfoEventArgs, ExperiencePreferencesEventArgs,
    GroupChatJoinedEventArgs, LoadUrlEventArgs, MeanCollisionEventArgs, MoneyBalanceReplyEventArgs,
    NavMeshStatusUpdateEventArgs, ProductInfoEventArgs, RegionExperiencesEventArgs,
    ScriptControlEventArgs, ScriptDialogEventArgs, ScriptQuestionEventArgs,
    ScriptSensorReplyEventArgs, SetDisplayNameReplyEventArgs, ViewerBenefitsEventArgs,
};
use crate::{
    AgentManagerAgentMovement, ChatSessionMember, ChatType, EffectType, Error, GridClient,
    InstantMessageDialog, InstantMessageOnline, LookAtType, MoneyTransactionType, MuteEntry,
    PointAtType, Simulator, TransactionFlags, network_manager::CapsEventQueueCallback,
};
use libremetaverse_structured_data::{OSD, OSDMap, OSDParser};
use libremetaverse_types::compat::{EventHandler, Subscription, Uri};
use libremetaverse_types::{Color4, UUID, Vector3, Vector3d, Vector4};
use std::collections::{BTreeMap, HashMap};
use std::fmt;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, OnceLock, RwLock};
use std::time::{Duration, Instant, UNIX_EPOCH};

const MAX_CHAT_BYTES: usize = 1023;
const MAX_DIALOG_LABEL_BYTES: usize = 254;
const MAX_GESTURE_ASSET_BYTES: usize = 16 * 1024 * 1024;
const MAX_MUTE_LIST_BYTES: usize = 16 * 1024 * 1024;
const GESTURE_TIMEOUT: Duration = Duration::from_secs(30);

struct MuteXfer {
    announced_size: Option<usize>,
    bytes: Vec<u8>,
    next_packet: u32,
    started: Instant,
}

impl Default for MuteXfer {
    fn default() -> Self {
        Self {
            announced_size: None,
            bytes: Vec::new(),
            next_packet: 0,
            started: Instant::now(),
        }
    }
}

fn mutex<T>(value: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
    value
        .lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner)
}

fn send_simulator_packet(
    simulator: &Simulator,
    packet_type: crate::packets::PacketType,
    data: Vec<u8>,
) -> Result<(), Error> {
    let length = i32::try_from(data.len()).map_err(|_| Error::Argument)?;
    let zerocoded = data
        .first()
        .is_some_and(|flags| flags & crate::Helpers::MSG_ZEROCODED != 0);
    simulator.native_send_packet_data(data, length, packet_type, zerocoded)
}

fn parse_mute_list(text: &str) -> HashMap<String, MuteEntry> {
    let mut result = HashMap::new();
    for line in text.lines().map(str::trim).filter(|line| !line.is_empty()) {
        let mut fields = line.splitn(3, char::is_whitespace);
        let (Some(type_text), Some(id_text), Some(name_and_flags)) =
            (fields.next(), fields.next(), fields.next())
        else {
            continue;
        };
        let Ok(type_number) = type_text.parse::<i32>() else {
            continue;
        };
        let type_ = match type_number {
            0 => crate::MuteType::ByName,
            1 => crate::MuteType::Resident,
            2 => crate::MuteType::Object,
            3 => crate::MuteType::Group,
            4 => crate::MuteType::External,
            _ => continue,
        };
        let Ok(id) = UUID::new_with_string(id_text.to_owned()) else {
            continue;
        };
        let (name, flags) = name_and_flags
            .rsplit_once('|')
            .map_or((name_and_flags, 0), |(name, flags)| {
                (name, flags.trim().parse::<i32>().unwrap_or(0))
            });
        let name = name.trim().to_owned();
        if name.is_empty() {
            continue;
        }
        result.insert(
            format!("{id}|{name}"),
            MuteEntry {
                flags: crate::MuteFlags(flags),
                id,
                name,
                type_,
            },
        );
    }
    result
}

pub(crate) struct EventRegistryState<T> {
    next_id: AtomicU64,
    handlers: Mutex<Vec<(u64, EventHandler<T>)>>,
}

pub(crate) struct EventRegistry<T> {
    state: Arc<EventRegistryState<T>>,
}

impl<T> Default for EventRegistry<T> {
    fn default() -> Self {
        Self {
            state: Arc::new(EventRegistryState {
                next_id: AtomicU64::new(1),
                handlers: Mutex::new(Vec::new()),
            }),
        }
    }
}

impl<T: Clone + 'static> EventRegistry<T> {
    pub(crate) fn subscribe(&self, handler: EventHandler<T>) -> Subscription {
        let id = self.state.next_id.fetch_add(1, Ordering::Relaxed);
        mutex(&self.state.handlers).push((id, handler));
        let state = Arc::downgrade(&self.state);
        Subscription::new(move || {
            if let Some(state) = state.upgrade() {
                mutex(&state.handlers).retain(|(candidate, _)| *candidate != id);
            }
        })
    }

    pub(crate) fn emit(&self, value: T) {
        let handlers: Vec<_> = mutex(&self.state.handlers)
            .iter()
            .map(|(_, handler)| Arc::clone(handler))
            .collect();
        for handler in handlers {
            let argument = value.clone();
            let _ = catch_unwind(AssertUnwindSafe(|| handler(argument)));
        }
    }
}

#[derive(Clone)]
pub struct InstantMessage {
    pub binary_bucket: Vec<u8>,
    pub dialog: InstantMessageDialog,
    pub from_agent_id: UUID,
    pub from_agent_name: String,
    pub group_im: bool,
    pub im_session_id: UUID,
    pub message: String,
    pub offline: InstantMessageOnline,
    pub parent_estate_id: u32,
    pub position: Vector3,
    pub region_id: UUID,
    pub timestamp: std::time::SystemTime,
    pub to_agent_id: UUID,
}

impl fmt::Debug for InstantMessage {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("InstantMessage")
            .field("dialog", &self.dialog)
            .field("from_agent_id", &self.from_agent_id)
            .field("group_im", &self.group_im)
            .field("offline", &self.offline)
            .field("parent_estate_id", &self.parent_estate_id)
            .field("region_id", &self.region_id)
            .field("timestamp", &self.timestamp)
            .field("to_agent_id", &self.to_agent_id)
            .field("message_len", &self.message.len())
            .field("binary_bucket_len", &self.binary_bucket.len())
            .finish_non_exhaustive()
    }
}

impl InstantMessage {
    #[must_use]
    pub fn to_string(&self) -> String {
        format!("{}: {}", self.from_agent_name, self.message)
    }
}

#[derive(Clone)]
pub struct InstantMessageEventArgs {
    im: InstantMessage,
    simulator: Option<Simulator>,
}

#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct ProductInfoRequestMessageProductInfo {
    pub description: String,
    pub name: String,
    pub sku: String,
}

impl ProductInfoRequestMessageProductInfo {
    pub fn new() -> Result<Self, Error> {
        Ok(Self::default())
    }
}

#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct ProductInfoRequestMessage {
    pub products: Vec<ProductInfoRequestMessageProductInfo>,
}

impl ProductInfoRequestMessage {
    pub fn new() -> Result<Self, Error> {
        Ok(Self::default())
    }

    pub fn deserialize(&mut self, map: OSDMap) -> Result<(), Error> {
        self.products.clear();
        let Some(OSD::Array(products)) = map.get("products") else {
            return Ok(());
        };
        for product in products {
            let OSD::Map(product) = product else {
                continue;
            };
            self.products.push(ProductInfoRequestMessageProductInfo {
                description: product
                    .get("description")
                    .map_or(Ok(String::new()), OSD::as_string)?,
                name: product
                    .get("name")
                    .map_or(Ok(String::new()), OSD::as_string)?,
                sku: product
                    .get("sku")
                    .map_or(Ok(String::new()), OSD::as_string)?,
            });
        }
        Ok(())
    }

    pub fn serialize(&self) -> Result<OSDMap, Error> {
        OSDMap::new_with_dictionary(HashMap::from([(
            "products".into(),
            OSD::Array(
                self.products
                    .iter()
                    .map(|product| {
                        OSD::Map(HashMap::from([
                            ("sku".into(), OSD::String(product.sku.clone())),
                            ("name".into(), OSD::String(product.name.clone())),
                            (
                                "description".into(),
                                OSD::String(product.description.clone()),
                            ),
                        ]))
                    })
                    .collect(),
            ),
        )]))
    }
}

impl crate::interfaces::IMessage for ProductInfoRequestMessage {
    fn deserialize(&mut self, map: OSDMap) -> Result<(), Error> {
        Self::deserialize(self, map)
    }

    fn serialize(&self) -> Result<OSDMap, Error> {
        Self::serialize(self)
    }
}

impl InstantMessageEventArgs {
    pub fn new(im: InstantMessage, simulator: Option<Simulator>) -> Result<Self, Error> {
        Ok(Self { im, simulator })
    }

    #[must_use]
    pub fn im(&self) -> InstantMessage {
        self.im.clone()
    }

    #[must_use]
    pub fn simulator(&self) -> Option<Simulator> {
        self.simulator.clone()
    }
}

pub(crate) struct AgentManagerInner {
    disposed: AtomicBool,
    agent_id: RwLock<UUID>,
    session_id: RwLock<UUID>,
    secure_session_id: RwLock<UUID>,
    first_name: RwLock<String>,
    last_name: RwLock<String>,
    start_location: RwLock<String>,
    look_at: RwLock<Vector3>,
    home_region_handle: RwLock<u64>,
    home_look_at: RwLock<Vector3>,
    local_id: RwLock<u32>,
    collision_plane: RwLock<Vector4>,
    health: RwLock<f32>,
    height_width_generation: AtomicU32,
    active_group: RwLock<UUID>,
    active_group_powers: RwLock<crate::GroupPowers>,
    agent_access: RwLock<String>,
    hover_height: RwLock<f32>,
    balance: RwLock<i32>,
    active_gestures: RwLock<HashMap<UUID, UUID>>,
    signaled_animations: Arc<RwLock<HashMap<UUID, i32>>>,
    mute_list: Arc<RwLock<HashMap<String, MuteEntry>>>,
    mute_xfers: Mutex<HashMap<u64, MuteXfer>>,
    instant_message: EventRegistry<InstantMessageEventArgs>,
    agent_data_reply: EventRegistry<AgentDataReplyEventArgs>,
    animations_changed: EventRegistry<AnimationsChangedEventArgs>,
    agent_experiences: RwLock<Option<ExperienceListMessage>>,
    experience_preferences: RwLock<Option<ExperiencePreferencesMessage>>,
    region_experiences: RwLock<Option<RegionExperiencesMessage>>,
    agent_preferences: RwLock<Option<AgentPreferencesMessage>>,
    avatar_render_info: RwLock<Option<AvatarRenderInfoMessage>>,
    viewer_benefits: RwLock<Option<ViewerBenefitsMessage>>,
    benefits: RwLock<Option<crate::AccountLevelBenefits>>,
    last_nav_mesh_status: RwLock<Option<crate::messages::linden::NavMeshStatusUpdateMessage>>,
    product_info: RwLock<Option<ProductInfoRequestMessage>>,
    group_chat_sessions: Arc<RwLock<HashMap<UUID, Vec<ChatSessionMember>>>>,
    network: Mutex<Option<crate::NetworkManager>>,
    raw_packet_subscription: Mutex<Option<Subscription>>,
    caps_subscriptions: Mutex<Vec<Subscription>>,
    login_callback: Mutex<Option<crate::NetworkManagerLoginResponseCallback>>,
    movement: OnceLock<Arc<crate::agent_movement::AgentMovementRuntime>>,
    chat_from_simulator: EventRegistry<ChatEventArgs>,
    money_balance: EventRegistry<BalanceEventArgs>,
    money_balance_reply: EventRegistry<MoneyBalanceReplyEventArgs>,
    agent_experiences_updated: EventRegistry<AgentExperiencesEventArgs>,
    experience_preferences_updated: EventRegistry<ExperiencePreferencesEventArgs>,
    region_experiences_updated: EventRegistry<RegionExperiencesEventArgs>,
    experience_info_received: EventRegistry<ExperienceInfoEventArgs>,
    agent_preferences_updated: EventRegistry<AgentPreferencesEventArgs>,
    avatar_render_info_updated: EventRegistry<AvatarRenderInfoEventArgs>,
    viewer_benefits_updated: EventRegistry<ViewerBenefitsEventArgs>,
    product_info_updated: EventRegistry<ProductInfoEventArgs>,
    group_chat_joined: EventRegistry<GroupChatJoinedEventArgs>,
    chat_session_member_added: EventRegistry<ChatSessionMemberAddedEventArgs>,
    chat_session_member_left: EventRegistry<ChatSessionMemberLeftEventArgs>,
    set_display_name_reply: EventRegistry<SetDisplayNameReplyEventArgs>,
    nav_mesh_status_update: EventRegistry<NavMeshStatusUpdateEventArgs>,
    mute_list_updated: EventRegistry<()>,
    alert_message: EventRegistry<AlertMessageEventArgs>,
    load_url: EventRegistry<LoadUrlEventArgs>,
    mean_collision: EventRegistry<MeanCollisionEventArgs>,
    script_control_change: EventRegistry<ScriptControlEventArgs>,
    script_dialog: EventRegistry<ScriptDialogEventArgs>,
    script_question: EventRegistry<ScriptQuestionEventArgs>,
    script_sensor_reply: EventRegistry<ScriptSensorReplyEventArgs>,
}

impl Default for AgentManagerInner {
    fn default() -> Self {
        Self {
            disposed: AtomicBool::new(false),
            agent_id: RwLock::new(UUID::zero()),
            session_id: RwLock::new(UUID::zero()),
            secure_session_id: RwLock::new(UUID::zero()),
            first_name: RwLock::new(String::new()),
            last_name: RwLock::new(String::new()),
            start_location: RwLock::new(String::new()),
            look_at: RwLock::new(Vector3::zero()),
            home_region_handle: RwLock::new(0),
            home_look_at: RwLock::new(Vector3::zero()),
            local_id: RwLock::new(0),
            collision_plane: RwLock::new(Vector4::zero()),
            health: RwLock::new(0.0),
            height_width_generation: AtomicU32::new(0),
            active_group: RwLock::new(UUID::zero()),
            active_group_powers: RwLock::new(crate::GroupPowers(0)),
            agent_access: RwLock::new(String::new()),
            hover_height: RwLock::new(0.0),
            balance: RwLock::new(0),
            active_gestures: RwLock::new(HashMap::new()),
            signaled_animations: Arc::new(RwLock::new(HashMap::new())),
            mute_list: Arc::new(RwLock::new(HashMap::new())),
            mute_xfers: Mutex::new(HashMap::new()),
            instant_message: EventRegistry::default(),
            agent_data_reply: EventRegistry::default(),
            animations_changed: EventRegistry::default(),
            agent_experiences: RwLock::new(None),
            experience_preferences: RwLock::new(None),
            region_experiences: RwLock::new(None),
            agent_preferences: RwLock::new(None),
            avatar_render_info: RwLock::new(None),
            viewer_benefits: RwLock::new(None),
            benefits: RwLock::new(None),
            last_nav_mesh_status: RwLock::new(None),
            product_info: RwLock::new(None),
            group_chat_sessions: Arc::new(RwLock::new(HashMap::new())),
            network: Mutex::new(None),
            raw_packet_subscription: Mutex::new(None),
            caps_subscriptions: Mutex::new(Vec::new()),
            login_callback: Mutex::new(None),
            movement: OnceLock::new(),
            chat_from_simulator: EventRegistry::default(),
            money_balance: EventRegistry::default(),
            money_balance_reply: EventRegistry::default(),
            agent_experiences_updated: EventRegistry::default(),
            experience_preferences_updated: EventRegistry::default(),
            region_experiences_updated: EventRegistry::default(),
            experience_info_received: EventRegistry::default(),
            agent_preferences_updated: EventRegistry::default(),
            avatar_render_info_updated: EventRegistry::default(),
            viewer_benefits_updated: EventRegistry::default(),
            product_info_updated: EventRegistry::default(),
            group_chat_joined: EventRegistry::default(),
            chat_session_member_added: EventRegistry::default(),
            chat_session_member_left: EventRegistry::default(),
            set_display_name_reply: EventRegistry::default(),
            nav_mesh_status_update: EventRegistry::default(),
            mute_list_updated: EventRegistry::default(),
            alert_message: EventRegistry::default(),
            load_url: EventRegistry::default(),
            mean_collision: EventRegistry::default(),
            script_control_change: EventRegistry::default(),
            script_dialog: EventRegistry::default(),
            script_question: EventRegistry::default(),
            script_sensor_reply: EventRegistry::default(),
        }
    }
}

impl AgentManagerInner {
    fn handle_raw_packet(&self, event: crate::network_manager::RawPacketReceivedEventArgs) {
        if let Some(movement) = self.movement.get()
            && movement
                .handle_raw_packet(event.packet_type, &event.data, event.simulator.clone())
                .unwrap_or(false)
        {
            return;
        }
        let result = match event.packet_type {
            crate::packets::PacketType::ChatFromSimulator => {
                self.handle_chat_packet(&event.data, event.simulator)
            }
            crate::packets::PacketType::ImprovedInstantMessage => {
                self.handle_instant_message_packet(&event.data, event.simulator)
            }
            crate::packets::PacketType::MoneyBalanceReply => {
                self.handle_money_balance_packet(&event.data)
            }
            crate::packets::PacketType::AgentDataUpdate => {
                self.handle_agent_data_update_packet(&event.data)
            }
            crate::packets::PacketType::AvatarAnimation => {
                self.handle_avatar_animation_packet(&event.data)
            }
            crate::packets::PacketType::AlertMessage => {
                self.handle_alert_message_packet(&event.data)
            }
            crate::packets::PacketType::AgentAlertMessage => {
                self.handle_agent_alert_message_packet(&event.data)
            }
            crate::packets::PacketType::HealthMessage => {
                self.handle_health_message_packet(&event.data)
            }
            crate::packets::PacketType::LoadURL => self.handle_load_url_packet(&event.data),
            crate::packets::PacketType::MeanCollisionAlert => {
                self.handle_mean_collision_packet(&event.data)
            }
            crate::packets::PacketType::ScriptControlChange => {
                self.handle_script_control_packet(&event.data)
            }
            crate::packets::PacketType::ScriptDialog => {
                self.handle_script_dialog_packet(&event.data)
            }
            crate::packets::PacketType::ScriptQuestion => {
                self.handle_script_question_packet(&event.data, event.simulator)
            }
            crate::packets::PacketType::ScriptSensorReply => {
                self.handle_script_sensor_reply_packet(&event.data)
            }
            crate::packets::PacketType::MuteListUpdate => {
                self.handle_mute_list_update_packet(&event.data, event.simulator)
            }
            crate::packets::PacketType::SendXferPacket => {
                self.handle_mute_xfer_packet(&event.data, event.simulator)
            }
            _ => Ok(()),
        };
        // Packet callbacks are intentionally fault-isolated, matching the C#
        // event pipeline. Malformed wire data is rejected without panicking or
        // poisoning the network receive worker.
        let _ = result;
    }

    fn handle_alert_message_packet(&self, data: &[u8]) -> Result<(), Error> {
        let packet = decode_raw_packet::<crate::packets::AlertMessagePacket>(data)?;
        let (notification_id, extra_params) =
            packet
                .alert_info
                .first()
                .map_or(Ok::<_, Error>((None, None)), |info| {
                    let extra_params = if info.extra_params.is_empty() {
                        None
                    } else {
                        match OSDParser::deserialize_with_bytes(info.extra_params.clone())? {
                            OSD::Map(map) => Some(OSDMap::new_with_dictionary(map)?),
                            _ => return Err(Error::InvalidOperation),
                        }
                    };
                    Ok((Some(wire_string(info.message.clone())), extra_params))
                })?;
        self.alert_message.emit(AlertMessageEventArgs::new(
            wire_string(packet.alert_data.message),
            notification_id,
            extra_params,
        )?);
        Ok(())
    }

    fn handle_agent_alert_message_packet(&self, data: &[u8]) -> Result<(), Error> {
        let packet = decode_raw_packet::<crate::packets::AgentAlertMessagePacket>(data)?;
        self.alert_message.emit(AlertMessageEventArgs::new(
            wire_string(packet.alert_data.message),
            None,
            None,
        )?);
        Ok(())
    }

    fn handle_health_message_packet(&self, data: &[u8]) -> Result<(), Error> {
        let packet = decode_raw_packet::<crate::packets::HealthMessagePacket>(data)?;
        *self
            .health
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = packet.health_data.health;
        Ok(())
    }

    fn handle_load_url_packet(&self, data: &[u8]) -> Result<(), Error> {
        let packet = decode_raw_packet::<crate::packets::LoadURLPacket>(data)?;
        let data = packet.data;
        self.load_url.emit(LoadUrlEventArgs::new(
            wire_string(data.object_name),
            data.object_id,
            data.owner_id,
            data.owner_is_group,
            wire_string(data.message),
            wire_string(data.url),
        )?);
        Ok(())
    }

    fn handle_mean_collision_packet(&self, data: &[u8]) -> Result<(), Error> {
        let packet = decode_raw_packet::<crate::packets::MeanCollisionAlertPacket>(data)?;
        for collision in packet.mean_collision {
            let type_ = match collision.type_ {
                0 => crate::MeanCollisionType::None,
                1 => crate::MeanCollisionType::Bump,
                2 => crate::MeanCollisionType::LLPushObject,
                3 => crate::MeanCollisionType::SelectedObjectCollide,
                4 => crate::MeanCollisionType::ScriptedObjectCollide,
                5 => crate::MeanCollisionType::PhysicalObjectCollide,
                _ => return Err(Error::Argument),
            };
            self.mean_collision.emit(MeanCollisionEventArgs::new(
                type_,
                collision.perp,
                collision.victim,
                collision.mag,
                UNIX_EPOCH + Duration::from_secs(u64::from(collision.time)),
            )?);
        }
        Ok(())
    }

    fn handle_script_control_packet(&self, data: &[u8]) -> Result<(), Error> {
        let packet = decode_raw_packet::<crate::packets::ScriptControlChangePacket>(data)?;
        for change in packet.data {
            self.script_control_change.emit(ScriptControlEventArgs::new(
                crate::ScriptControlChange(change.controls),
                change.pass_to_agent,
                change.take_controls,
            )?);
        }
        Ok(())
    }

    fn handle_script_dialog_packet(&self, data: &[u8]) -> Result<(), Error> {
        let packet = decode_raw_packet::<crate::packets::ScriptDialogPacket>(data)?;
        let owner_id = packet
            .owner_data
            .first()
            .map_or(UUID::zero(), |owner| owner.owner_id);
        let buttons = packet
            .buttons
            .into_iter()
            .map(|button| wire_string(button.button_label))
            .collect();
        let dialog = packet.data;
        self.script_dialog.emit(ScriptDialogEventArgs::new(
            wire_string(dialog.message),
            wire_string(dialog.object_name),
            dialog.image_id,
            dialog.object_id,
            wire_string(dialog.first_name),
            wire_string(dialog.last_name),
            dialog.chat_channel,
            buttons,
            owner_id,
        )?);
        Ok(())
    }

    fn handle_script_question_packet(
        &self,
        data: &[u8],
        simulator: Simulator,
    ) -> Result<(), Error> {
        let packet = decode_raw_packet::<crate::packets::ScriptQuestionPacket>(data)?;
        let question = packet.data;
        self.script_question.emit(ScriptQuestionEventArgs::new(
            simulator,
            question.task_id,
            question.item_id,
            wire_string(question.object_name),
            wire_string(question.object_owner),
            crate::ScriptPermission(question.questions),
        )?);
        Ok(())
    }

    fn handle_script_sensor_reply_packet(&self, data: &[u8]) -> Result<(), Error> {
        let packet = decode_raw_packet::<crate::packets::ScriptSensorReplyPacket>(data)?;
        let requester_id = packet.requester.source_id;
        for sensed in packet.sensed_data {
            self.script_sensor_reply
                .emit(ScriptSensorReplyEventArgs::new(
                    requester_id,
                    sensed.group_id,
                    wire_string(sensed.name),
                    sensed.object_id,
                    sensed.owner_id,
                    sensed.position,
                    sensed.range,
                    sensed.rotation,
                    crate::ScriptSensorTypeFlags(sensed.type_),
                    sensed.velocity,
                )?);
        }
        Ok(())
    }

    fn handle_mute_list_update_packet(
        &self,
        data: &[u8],
        simulator: Simulator,
    ) -> Result<(), Error> {
        let packet = decode_raw_packet::<crate::packets::MuteListUpdatePacket>(data)?;
        let expected_agent = *self
            .agent_id
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        if expected_agent != UUID::zero() && packet.mute_data.agent_id != expected_agent {
            return Ok(());
        }

        let filename = wire_string(packet.mute_data.filename);
        let xfer_id = UUID::random()?.get_u_long()?;
        {
            let mut transfers = mutex(&self.mute_xfers);
            transfers.retain(|_, transfer| transfer.started.elapsed() < Duration::from_mins(1));
            transfers.insert(xfer_id, MuteXfer::default());
        }

        let mut request = crate::packets::RequestXferPacket::new_with_constructor()?;
        request.xfer_id.id = xfer_id;
        request.xfer_id.filename = filename.into_bytes();
        request.xfer_id.file_path = 4;
        request.xfer_id.delete_on_completion = true;
        request.xfer_id.use_big_packets = false;
        request.xfer_id.v_file_id = UUID::zero();
        request.xfer_id.v_file_type = libremetaverse_types::AssetType::Unknown as i16;
        send_simulator_packet(
            &simulator,
            crate::packets::PacketType::RequestXfer,
            request.to_bytes_with_method()?,
        )?;

        Ok(())
    }

    fn handle_mute_xfer_packet(&self, data: &[u8], simulator: Simulator) -> Result<(), Error> {
        let packet = decode_raw_packet::<crate::packets::SendXferPacketPacket>(data)?;
        let xfer_id = packet.xfer_id.id;
        let packet_number = packet.xfer_id.packet & 0x0fff_ffff;
        let is_last = packet.xfer_id.packet & 0x8000_0000 != 0;
        let mut completed = None;
        let confirmation;
        {
            let mut transfers = mutex(&self.mute_xfers);
            let Some(transfer) = transfers.get_mut(&xfer_id) else {
                return Ok(());
            };
            if packet_number != transfer.next_packet {
                confirmation = transfer.next_packet.saturating_sub(1);
            } else {
                confirmation = packet_number;
                let mut incoming = packet.data_packet.data;
                if packet_number == 0 {
                    let size_bytes: [u8; 4] = incoming
                        .get(..4)
                        .ok_or(Error::InvalidOperation)?
                        .try_into()
                        .map_err(|_| Error::InvalidOperation)?;
                    let size = usize::try_from(u32::from_le_bytes(size_bytes))
                        .map_err(|_| Error::Argument)?;
                    if size > MAX_MUTE_LIST_BYTES {
                        transfers.remove(&xfer_id);
                        return Err(Error::Argument);
                    }
                    transfer.bytes = Vec::with_capacity(size);
                    transfer.announced_size = Some(size);
                    incoming.drain(..4);
                }
                let size = transfer.announced_size.ok_or(Error::InvalidOperation)?;
                if transfer.bytes.len().saturating_add(incoming.len()) > size {
                    transfers.remove(&xfer_id);
                    return Err(Error::InvalidOperation);
                }
                transfer.bytes.extend_from_slice(&incoming);
                transfer.next_packet = transfer.next_packet.saturating_add(1);
                if is_last {
                    if transfer.bytes.len() != size {
                        transfers.remove(&xfer_id);
                        return Err(Error::InvalidOperation);
                    }
                    completed = Some(std::mem::take(&mut transfer.bytes));
                    transfers.remove(&xfer_id);
                }
            }
        }

        let mut confirm = crate::packets::ConfirmXferPacketPacket::new_with_constructor()?;
        confirm.xfer_id.id = xfer_id;
        confirm.xfer_id.packet = confirmation;
        send_simulator_packet(
            &simulator,
            crate::packets::PacketType::ConfirmXferPacket,
            confirm.to_bytes_with_method()?,
        )?;

        if let Some(bytes) = completed {
            let parsed = parse_mute_list(&wire_string(bytes));
            *self
                .mute_list
                .write()
                .unwrap_or_else(std::sync::PoisonError::into_inner) = parsed;
            self.mute_list_updated.emit(());
        }
        Ok(())
    }

    fn handle_chat_packet(&self, data: &[u8], simulator: Simulator) -> Result<(), Error> {
        let packet = decode_raw_packet::<crate::packets::ChatFromSimulatorPacket>(data)?;
        let chat = packet.chat_data;
        self.chat_from_simulator.emit(ChatEventArgs::new(
            simulator,
            wire_string(chat.message),
            chat_audible_level(chat.audible)?,
            chat_type(chat.chat_type)?,
            chat_source_type(chat.source_type)?,
            wire_string(chat.from_name),
            chat.source_id,
            chat.owner_id,
            chat.position,
        )?);
        Ok(())
    }

    fn handle_instant_message_packet(
        &self,
        data: &[u8],
        simulator: Simulator,
    ) -> Result<(), Error> {
        let packet = decode_raw_packet::<crate::packets::ImprovedInstantMessagePacket>(data)?;
        let block = packet.message_block;
        if block.from_group {
            self.group_chat_sessions
                .write()
                .unwrap_or_else(std::sync::PoisonError::into_inner)
                .entry(block.id)
                .or_default();
        }
        self.instant_message.emit(InstantMessageEventArgs::new(
            InstantMessage {
                binary_bucket: block.binary_bucket,
                dialog: instant_message_dialog(block.dialog)?,
                from_agent_id: packet.agent_data.agent_id,
                from_agent_name: wire_string(block.from_agent_name),
                group_im: block.from_group,
                im_session_id: block.id,
                message: wire_string(block.message),
                offline: instant_message_online(block.offline)?,
                parent_estate_id: block.parent_estate_id,
                position: block.position,
                region_id: block.region_id,
                timestamp: UNIX_EPOCH + Duration::from_secs(u64::from(block.timestamp)),
                to_agent_id: block.to_agent_id,
            },
            Some(simulator),
        )?);
        Ok(())
    }

    fn handle_money_balance_packet(&self, data: &[u8]) -> Result<(), Error> {
        let packet = decode_raw_packet::<crate::packets::MoneyBalanceReplyPacket>(data)?;
        let money = packet.money_data;
        let transaction = packet.transaction_info;
        *self
            .balance
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = money.money_balance;
        self.money_balance_reply
            .emit(MoneyBalanceReplyEventArgs::new(
                money.transaction_id,
                money.transaction_success,
                money.money_balance,
                money.square_meters_credit,
                money.square_meters_committed,
                wire_string(money.description),
                crate::TransactionInfo {
                    amount: transaction.amount,
                    dest_id: transaction.dest_id,
                    is_dest_group: transaction.is_dest_group,
                    is_source_group: transaction.is_source_group,
                    item_description: wire_string(transaction.item_description),
                    source_id: transaction.source_id,
                    transaction_type: transaction.transaction_type,
                },
            )?);
        self.money_balance
            .emit(BalanceEventArgs::new(money.money_balance)?);
        Ok(())
    }

    fn handle_agent_data_update_packet(&self, data: &[u8]) -> Result<(), Error> {
        let packet = decode_raw_packet::<crate::packets::AgentDataUpdatePacket>(data)?;
        let update = packet.agent_data;
        let expected_id = self.expected_agent_id();
        if expected_id != UUID::zero() && update.agent_id != expected_id {
            return Ok(());
        }
        let first_name = wire_string(update.first_name);
        let last_name = wire_string(update.last_name);
        let group_title = wire_string(update.group_title);
        let group_name = wire_string(update.group_name);
        let group_powers = crate::GroupPowers(update.group_powers);
        self.first_name
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .clone_from(&first_name);
        self.last_name
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .clone_from(&last_name);
        *self
            .active_group
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = update.active_group_id;
        *self
            .active_group_powers
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = group_powers;
        self.agent_data_reply.emit(AgentDataReplyEventArgs::new(
            first_name,
            last_name,
            update.active_group_id,
            group_title,
            group_powers,
            group_name,
        )?);
        Ok(())
    }

    fn handle_avatar_animation_packet(&self, data: &[u8]) -> Result<(), Error> {
        let packet = decode_raw_packet::<crate::packets::AvatarAnimationPacket>(data)?;
        let expected_id = self.expected_agent_id();
        if expected_id != UUID::zero() && packet.sender.id != expected_id {
            return Ok(());
        }
        let animations = packet
            .animation_list
            .into_iter()
            .map(|entry| (entry.anim_id, entry.anim_sequence_id))
            .collect::<HashMap<_, _>>();
        self.signaled_animations
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .clone_from(&animations);
        self.animations_changed
            .emit(AnimationsChangedEventArgs::new(animations)?);
        Ok(())
    }

    fn expected_agent_id(&self) -> UUID {
        let configured_id = *self
            .agent_id
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        if configured_id == UUID::zero() {
            mutex(&self.network)
                .as_ref()
                .map_or(UUID::zero(), crate::NetworkManager::native_agent_id)
        } else {
            configured_id
        }
    }

    pub(crate) fn native_ids(&self, network: &crate::NetworkManager) -> (UUID, UUID) {
        let agent_id = *self
            .agent_id
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        let session_id = *self
            .session_id
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        (
            if agent_id == UUID::zero() {
                network.native_agent_id()
            } else {
                agent_id
            },
            if session_id == UUID::zero() {
                network.native_session_id()
            } else {
                session_id
            },
        )
    }

    fn apply_login_response(&self, response: &crate::LoginResponseData) {
        *self
            .agent_id
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = response.agent_id();
        *self
            .session_id
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = response.session_id();
        *self
            .secure_session_id
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = response.secure_session_id();
        *self
            .first_name
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = response.first_name();
        *self
            .last_name
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = response.last_name();
        *self
            .start_location
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = response.start_location();
        *self
            .agent_access
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = response.agent_access();
        let look_at = response.look_at();
        *self
            .look_at
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = look_at;
        let home = response.home();
        *self
            .home_region_handle
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = home.region_handle;
        *self
            .home_look_at
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = home.look_at;
        *self
            .benefits
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = response.account_level_benefits();
        *self
            .active_gestures
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = response.gestures();
        if let Some(movement) = self.movement.get() {
            movement.set_home_position(home.position);
            let _ = movement.set_login_look_at(look_at);
        }
    }

    pub(crate) fn update_avatar_object_state(&self, local_id: u32, collision_plane: Vector4) {
        *self
            .local_id
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = local_id;
        *self
            .collision_plane
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = collision_plane;
    }

    pub(crate) fn avatar_local_id(&self) -> u32 {
        *self
            .local_id
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
    }

    fn handle_caps_event(
        self: &Arc<Self>,
        client: Arc<GridClient>,
        name: &str,
        message: &dyn crate::interfaces::IMessage,
        simulator: Simulator,
    ) {
        if let Some(movement) = self.movement.get()
            && movement
                .handle_caps_event(name, message, simulator.clone())
                .unwrap_or(false)
        {
            return;
        }
        let result = match name {
            "ChatterBoxInvitation" => self.handle_chat_invitation(client, message, simulator),
            "ChatterBoxSessionEventReply" => self.handle_chat_session_event_reply(client, message),
            "ChatterBoxSessionStartReply" => self.handle_chat_session_start_reply(message),
            "ChatterBoxSessionAgentListUpdates" => self.handle_chat_session_agent_updates(message),
            "SetDisplayNameReply" => self.handle_set_display_name_reply(message),
            "NavMeshStatusUpdate" => self.handle_nav_mesh_status_update(message, simulator),
            _ => Ok(()),
        };
        // Event queue handlers are fault-isolated just like packet handlers.
        // A malformed event must not stop delivery of subsequent queue entries.
        let _ = result;
    }

    fn handle_chat_session_event_reply(
        &self,
        client: Arc<GridClient>,
        message: &dyn crate::interfaces::IMessage,
    ) -> Result<(), Error> {
        let mut reply = crate::messages::linden::ChatterboxSessionEventReplyMessage::new()?;
        reply.deserialize(message.serialize()?)?;
        if !reply.success {
            AgentManager::native_new(Some(client))?.native_join_group_chat(reply.session_id)?;
        }
        Ok(())
    }

    fn handle_chat_session_start_reply(
        &self,
        message: &dyn crate::interfaces::IMessage,
    ) -> Result<(), Error> {
        let mut reply = crate::messages::linden::ChatterBoxSessionStartReplyMessage::new()?;
        reply.deserialize(message.serialize()?)?;
        if reply.success {
            self.group_chat_sessions
                .write()
                .unwrap_or_else(std::sync::PoisonError::into_inner)
                .entry(reply.session_id)
                .or_default();
        }
        self.group_chat_joined.emit(GroupChatJoinedEventArgs::new(
            reply.session_id,
            reply.session_name,
            reply.temp_session_id,
            reply.success,
        )?);
        Ok(())
    }

    fn handle_chat_session_agent_updates(
        &self,
        message: &dyn crate::interfaces::IMessage,
    ) -> Result<(), Error> {
        let mut update = crate::messages::linden::ChatterBoxSessionAgentListUpdatesMessage::new()?;
        update.deserialize(message.serialize()?)?;
        let session_id = update.session_id;
        let mut added = Vec::new();
        let mut left = Vec::new();
        {
            let mut sessions = self
                .group_chat_sessions
                .write()
                .unwrap_or_else(std::sync::PoisonError::into_inner);
            let members = sessions.entry(session_id).or_default();
            for block in update.updates {
                let found = members
                    .iter()
                    .position(|member| member.avatar_key == block.agent_id);
                match block.transition.as_str() {
                    "ENTER" if found.is_none() => added.push(block.agent_id),
                    "LEAVE" => {
                        if let Some(index) = found {
                            members.remove(index);
                        }
                        left.push(block.agent_id);
                        continue;
                    }
                    _ => {}
                }
                let member = ChatSessionMember {
                    avatar_key: block.agent_id,
                    can_voice_chat: block.can_voice_chat,
                    is_moderator: block.is_moderator,
                    mute_text: block.mute_text,
                    mute_voice: block.mute_voice,
                };
                if let Some(index) = found {
                    members[index] = member;
                } else if block.transition == "ENTER" {
                    members.push(member);
                }
            }
        }
        for agent_id in added {
            self.chat_session_member_added
                .emit(ChatSessionMemberAddedEventArgs::new(session_id, agent_id)?);
        }
        for agent_id in left {
            self.chat_session_member_left
                .emit(ChatSessionMemberLeftEventArgs::new(session_id, agent_id)?);
        }
        Ok(())
    }

    fn handle_chat_invitation(
        self: &Arc<Self>,
        client: Arc<GridClient>,
        message: &dyn crate::interfaces::IMessage,
        simulator: Simulator,
    ) -> Result<(), Error> {
        let mut invitation = crate::messages::linden::ChatterBoxInvitationMessage::new()?;
        invitation.deserialize(message.serialize()?)?;
        // The reference deliberately ignores voice-only invitations.
        if invitation.voice {
            return Ok(());
        }
        self.group_chat_sessions
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .entry(invitation.im_session_id)
            .or_default();
        let args = InstantMessageEventArgs::new(
            InstantMessage {
                binary_bucket: invitation.binary_bucket,
                dialog: invitation.dialog,
                from_agent_id: invitation.from_agent_id,
                from_agent_name: invitation.from_agent_name,
                group_im: invitation.group_im,
                im_session_id: invitation.im_session_id,
                message: invitation.message,
                offline: invitation.offline,
                parent_estate_id: invitation.parent_estate_id,
                position: invitation.position,
                region_id: invitation.region_id,
                timestamp: invitation.timestamp,
                to_agent_id: invitation.to_agent_id,
            },
            Some(simulator),
        )?;
        let session_id = args.im().im_session_id;
        let inner = Arc::clone(self);
        let fallback_args = args.clone();
        let spawned = std::thread::Builder::new()
            .name("libremetaverse-chat-invitation".into())
            .spawn(move || {
                if let (Ok(runtime), Ok(manager)) = (
                    tokio::runtime::Builder::new_current_thread()
                        .enable_all()
                        .build(),
                    AgentManager::native_new(Some(client)),
                ) {
                    // The C# handler catches acceptance failures and still
                    // delivers the invitation to subscribers afterwards.
                    let _ = runtime
                        .block_on(manager.native_chatter_box_accept_invite(session_id, None));
                }
                inner.instant_message.emit(args);
            });
        if spawned.is_err() {
            self.instant_message.emit(fallback_args);
        }
        Ok(())
    }

    fn handle_set_display_name_reply(
        &self,
        message: &dyn crate::interfaces::IMessage,
    ) -> Result<(), Error> {
        let mut reply = crate::messages::linden::SetDisplayNameReplyMessage::new()?;
        reply.deserialize(message.serialize()?)?;
        self.set_display_name_reply
            .emit(SetDisplayNameReplyEventArgs::new(
                reply.status,
                reply.reason,
                reply.display_name,
            )?);
        Ok(())
    }

    fn handle_nav_mesh_status_update(
        &self,
        message: &dyn crate::interfaces::IMessage,
        simulator: Simulator,
    ) -> Result<(), Error> {
        let mut update = crate::messages::linden::NavMeshStatusUpdateMessage::new()?;
        update.deserialize(message.serialize()?)?;
        *self
            .last_nav_mesh_status
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(update.clone());
        self.nav_mesh_status_update
            .emit(NavMeshStatusUpdateEventArgs::new(update, simulator)?);
        Ok(())
    }
}

/// Native owner for the non-movement `AgentManager` surface.
pub struct AgentManager {
    pub agent_state_status: Option<AgentStateUpdateMessage>,
    pub group_chat_sessions: Arc<RwLock<HashMap<UUID, Vec<ChatSessionMember>>>>,
    pub movement: AgentManagerAgentMovement,
    pub mute_list: Arc<RwLock<HashMap<String, MuteEntry>>>,
    pub signaled_animations: Arc<RwLock<HashMap<UUID, i32>>>,
    pub(crate) client: Arc<GridClient>,
    inner: Arc<AgentManagerInner>,
}

impl fmt::Debug for AgentManager {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("AgentManager")
            .field("disposed", &self.inner.disposed.load(Ordering::Acquire))
            .field(
                "group_chat_session_count",
                &self
                    .group_chat_sessions
                    .read()
                    .unwrap_or_else(std::sync::PoisonError::into_inner)
                    .len(),
            )
            .finish_non_exhaustive()
    }
}

impl AgentManager {
    pub(crate) fn native_new(client: Option<Arc<GridClient>>) -> Result<Self, Error> {
        let client = client.ok_or(Error::ArgumentNull)?;
        let inner = client.cached_agent_manager_inner().unwrap_or_else(|| {
            let inner = Arc::new(AgentManagerInner::default());
            client.cache_agent_manager_inner(&inner);
            inner
        });
        let network = {
            let mut network = mutex(&inner.network);
            network.get_or_insert_with(|| client.network()).clone()
        };
        {
            let mut subscription = mutex(&inner.raw_packet_subscription);
            if subscription.is_none() {
                let weak = Arc::downgrade(&inner);
                *subscription = Some(network.subscribe_raw_packet(Arc::new(move |event| {
                    if let Some(inner) = weak.upgrade() {
                        inner.handle_raw_packet(event);
                    }
                })));
            }
        }
        {
            let mut subscriptions = mutex(&inner.caps_subscriptions);
            if subscriptions.is_empty() {
                for event_name in [
                    "ChatterBoxInvitation",
                    "ChatterBoxSessionEventReply",
                    "ChatterBoxSessionStartReply",
                    "ChatterBoxSessionAgentListUpdates",
                    "SetDisplayNameReply",
                    "NavMeshStatusUpdate",
                    "TeleportFinish",
                    "TeleportFailed",
                    "CrossedRegion",
                ] {
                    let weak_inner = Arc::downgrade(&inner);
                    let weak_client = Arc::downgrade(&client);
                    subscriptions.push(network.subscribe_caps(
                        event_name.into(),
                        CapsEventQueueCallback::from_handler(move |name, message, simulator| {
                            if let (Some(inner), Some(client)) =
                                (weak_inner.upgrade(), weak_client.upgrade())
                            {
                                inner.handle_caps_event(client, name, message, simulator);
                            }
                        }),
                    ));
                }
            }
        }
        let movement_runtime = Arc::clone(inner.movement.get_or_init(|| {
            crate::agent_movement::AgentMovementRuntime::new(
                Arc::clone(&client),
                network.clone(),
                Arc::downgrade(&inner),
            )
        }));
        {
            let mut callback = mutex(&inner.login_callback);
            if callback.is_none() {
                let weak = Arc::downgrade(&inner);
                let registered = crate::NetworkManagerLoginResponseCallback::from_handler(
                    move |success, redirect, _message, _reason, response| {
                        if success
                            && !redirect
                            && let (Some(inner), Some(response)) = (weak.upgrade(), response)
                        {
                            inner.apply_login_response(&response);
                        }
                    },
                );
                network.native_register_login_response_callback(
                    registered.clone(),
                    Some(vec![
                        "agent_id".into(),
                        "session_id".into(),
                        "secure_session_id".into(),
                        "first_name".into(),
                        "last_name".into(),
                        "start_location".into(),
                        "agent_access".into(),
                        "look_at".into(),
                        "home".into(),
                        "gestures".into(),
                        "account_level_benefits".into(),
                    ]),
                )?;
                *callback = Some(registered);
            }
        }
        Ok(Self {
            agent_state_status: None,
            group_chat_sessions: Arc::clone(&inner.group_chat_sessions),
            movement: AgentManagerAgentMovement::from_runtime(movement_runtime),
            mute_list: Arc::clone(&inner.mute_list),
            signaled_animations: Arc::clone(&inner.signaled_animations),
            client,
            inner,
        })
    }

    pub(crate) fn native_subscribe_im(
        &self,
        handler: EventHandler<InstantMessageEventArgs>,
    ) -> Subscription {
        self.inner.instant_message.subscribe(handler)
    }

    pub(crate) fn native_subscribe_agent_data_reply(
        &self,
        handler: EventHandler<AgentDataReplyEventArgs>,
    ) -> Subscription {
        self.inner.agent_data_reply.subscribe(handler)
    }

    pub(crate) fn native_subscribe_animations_changed(
        &self,
        handler: EventHandler<AnimationsChangedEventArgs>,
    ) -> Subscription {
        self.inner.animations_changed.subscribe(handler)
    }

    pub(crate) fn native_subscribe_alert_message(
        &self,
        handler: EventHandler<AlertMessageEventArgs>,
    ) -> Subscription {
        self.inner.alert_message.subscribe(handler)
    }

    pub(crate) fn native_subscribe_load_url(
        &self,
        handler: EventHandler<LoadUrlEventArgs>,
    ) -> Subscription {
        self.inner.load_url.subscribe(handler)
    }

    pub(crate) fn native_subscribe_mean_collision(
        &self,
        handler: EventHandler<MeanCollisionEventArgs>,
    ) -> Subscription {
        self.inner.mean_collision.subscribe(handler)
    }

    pub(crate) fn native_subscribe_script_control_change(
        &self,
        handler: EventHandler<ScriptControlEventArgs>,
    ) -> Subscription {
        self.inner.script_control_change.subscribe(handler)
    }

    pub(crate) fn native_subscribe_script_dialog(
        &self,
        handler: EventHandler<ScriptDialogEventArgs>,
    ) -> Subscription {
        self.inner.script_dialog.subscribe(handler)
    }

    pub(crate) fn native_subscribe_script_question(
        &self,
        handler: EventHandler<ScriptQuestionEventArgs>,
    ) -> Subscription {
        self.inner.script_question.subscribe(handler)
    }

    pub(crate) fn native_subscribe_script_sensor_reply(
        &self,
        handler: EventHandler<ScriptSensorReplyEventArgs>,
    ) -> Subscription {
        self.inner.script_sensor_reply.subscribe(handler)
    }

    pub(crate) fn native_dispose(&self) -> Result<(), Error> {
        if self.inner.disposed.swap(true, Ordering::AcqRel) {
            return Ok(());
        }
        mutex(&self.inner.raw_packet_subscription).take();
        mutex(&self.inner.caps_subscriptions).clear();
        if let Some(callback) = mutex(&self.inner.login_callback).take()
            && let Some(network) = mutex(&self.inner.network).as_ref()
        {
            network.native_unregister_login_response_callback(&callback)?;
        }
        if let Some(movement) = self.inner.movement.get() {
            movement.stop();
        }
        mutex(&self.inner.network).take();
        mutex(&self.inner.mute_xfers).clear();
        self.group_chat_sessions
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .clear();
        self.mute_list
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .clear();
        self.inner
            .active_gestures
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .clear();
        self.signaled_animations
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .clear();
        Ok(())
    }

    pub(crate) fn native_subscribe_chat_from_simulator(
        &self,
        handler: EventHandler<ChatEventArgs>,
    ) -> Subscription {
        self.inner.chat_from_simulator.subscribe(handler)
    }

    pub(crate) fn native_subscribe_money_balance(
        &self,
        handler: EventHandler<BalanceEventArgs>,
    ) -> Subscription {
        self.inner.money_balance.subscribe(handler)
    }

    pub(crate) fn native_subscribe_money_balance_reply(
        &self,
        handler: EventHandler<MoneyBalanceReplyEventArgs>,
    ) -> Subscription {
        self.inner.money_balance_reply.subscribe(handler)
    }

    pub(crate) fn native_subscribe_agent_experiences_updated(
        &self,
        handler: EventHandler<AgentExperiencesEventArgs>,
    ) -> Subscription {
        self.inner.agent_experiences_updated.subscribe(handler)
    }

    pub(crate) fn native_subscribe_experience_preferences_updated(
        &self,
        handler: EventHandler<ExperiencePreferencesEventArgs>,
    ) -> Subscription {
        self.inner.experience_preferences_updated.subscribe(handler)
    }

    pub(crate) fn native_subscribe_region_experiences_updated(
        &self,
        handler: EventHandler<RegionExperiencesEventArgs>,
    ) -> Subscription {
        self.inner.region_experiences_updated.subscribe(handler)
    }

    pub(crate) fn native_subscribe_experience_info_received(
        &self,
        handler: EventHandler<ExperienceInfoEventArgs>,
    ) -> Subscription {
        self.inner.experience_info_received.subscribe(handler)
    }

    pub(crate) fn native_subscribe_agent_preferences_updated(
        &self,
        handler: EventHandler<AgentPreferencesEventArgs>,
    ) -> Subscription {
        self.inner.agent_preferences_updated.subscribe(handler)
    }

    pub(crate) fn native_subscribe_avatar_render_info_updated(
        &self,
        handler: EventHandler<AvatarRenderInfoEventArgs>,
    ) -> Subscription {
        self.inner.avatar_render_info_updated.subscribe(handler)
    }

    pub(crate) fn native_subscribe_viewer_benefits_updated(
        &self,
        handler: EventHandler<ViewerBenefitsEventArgs>,
    ) -> Subscription {
        self.inner.viewer_benefits_updated.subscribe(handler)
    }

    pub(crate) fn native_subscribe_product_info_updated(
        &self,
        handler: EventHandler<ProductInfoEventArgs>,
    ) -> Subscription {
        self.inner.product_info_updated.subscribe(handler)
    }

    pub(crate) fn native_subscribe_mute_list_updated(
        &self,
        handler: EventHandler<()>,
    ) -> Subscription {
        self.inner.mute_list_updated.subscribe(handler)
    }

    pub(crate) fn native_subscribe_group_chat_joined(
        &self,
        handler: EventHandler<GroupChatJoinedEventArgs>,
    ) -> Subscription {
        self.inner.group_chat_joined.subscribe(handler)
    }

    pub(crate) fn native_subscribe_chat_session_member_added(
        &self,
        handler: EventHandler<ChatSessionMemberAddedEventArgs>,
    ) -> Subscription {
        self.inner.chat_session_member_added.subscribe(handler)
    }

    pub(crate) fn native_subscribe_chat_session_member_left(
        &self,
        handler: EventHandler<ChatSessionMemberLeftEventArgs>,
    ) -> Subscription {
        self.inner.chat_session_member_left.subscribe(handler)
    }

    pub(crate) fn native_subscribe_set_display_name_reply(
        &self,
        handler: EventHandler<SetDisplayNameReplyEventArgs>,
    ) -> Subscription {
        self.inner.set_display_name_reply.subscribe(handler)
    }

    pub(crate) fn native_subscribe_nav_mesh_status_update(
        &self,
        handler: EventHandler<NavMeshStatusUpdateEventArgs>,
    ) -> Subscription {
        self.inner.nav_mesh_status_update.subscribe(handler)
    }

    pub(crate) fn native_last_nav_mesh_status(
        &self,
    ) -> Option<crate::messages::linden::NavMeshStatusUpdateMessage> {
        self.inner
            .last_nav_mesh_status
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .clone()
    }

    pub(crate) fn native_set_last_nav_mesh_status(
        &self,
        value: Option<crate::messages::linden::NavMeshStatusUpdateMessage>,
    ) {
        *self
            .inner
            .last_nav_mesh_status
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = value;
    }

    pub(crate) fn native_agent_experience_list(&self) -> Option<ExperienceListMessage> {
        self.inner
            .agent_experiences
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .clone()
    }

    pub(crate) fn native_set_agent_experience_list(&self, value: Option<ExperienceListMessage>) {
        *self
            .inner
            .agent_experiences
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = value;
    }

    pub(crate) fn native_experience_preferences(&self) -> Option<ExperiencePreferencesMessage> {
        self.inner
            .experience_preferences
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .clone()
    }

    pub(crate) fn native_set_experience_preferences(
        &self,
        value: Option<ExperiencePreferencesMessage>,
    ) {
        *self
            .inner
            .experience_preferences
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = value;
    }

    pub(crate) fn native_last_region_experiences(&self) -> Option<RegionExperiencesMessage> {
        self.inner
            .region_experiences
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .clone()
    }

    pub(crate) fn native_set_last_region_experiences(
        &self,
        value: Option<RegionExperiencesMessage>,
    ) {
        *self
            .inner
            .region_experiences
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = value;
    }

    pub(crate) fn native_avatar_render_info(&self) -> Option<AvatarRenderInfoMessage> {
        self.inner
            .avatar_render_info
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .clone()
    }

    pub(crate) fn native_set_avatar_render_info(&self, value: Option<AvatarRenderInfoMessage>) {
        *self
            .inner
            .avatar_render_info
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = value;
    }

    pub(crate) fn native_viewer_benefits(&self) -> Option<ViewerBenefitsMessage> {
        self.inner
            .viewer_benefits
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .clone()
    }

    pub(crate) fn native_benefits(&self) -> crate::AccountLevelBenefits {
        self.inner
            .benefits
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .clone()
            .or_else(|| self.client.network().native_account_level_benefits())
            .unwrap_or_else(|| {
                crate::AccountLevelBenefits::new_with_osd_map(
                    OSDMap::new_with_dictionary(HashMap::new()).expect("an empty OSD map is valid"),
                )
                .expect("an empty benefit map is valid")
            })
    }

    pub(crate) fn native_set_benefits(&self, value: crate::AccountLevelBenefits) {
        *self
            .inner
            .benefits
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value);
    }

    pub(crate) fn native_set_viewer_benefits(&self, value: Option<ViewerBenefitsMessage>) {
        *self
            .inner
            .viewer_benefits
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = value;
    }

    pub(crate) fn native_product_info(&self) -> Option<ProductInfoRequestMessage> {
        self.inner
            .product_info
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .clone()
    }

    pub(crate) fn native_set_product_info(&self, value: Option<ProductInfoRequestMessage>) {
        *self
            .inner
            .product_info
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = value;
    }

    pub(crate) fn native_first_name(&self) -> String {
        self.inner
            .first_name
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .clone()
    }

    pub(crate) fn native_set_first_name(&self, value: String) {
        *self
            .inner
            .first_name
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = value;
    }

    pub(crate) fn native_secure_session_id(&self) -> UUID {
        *self
            .inner
            .secure_session_id
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
    }

    pub(crate) fn native_set_secure_session_id(&self, value: UUID) {
        *self
            .inner
            .secure_session_id
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = value;
    }

    pub(crate) fn native_start_location(&self) -> String {
        self.inner
            .start_location
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .clone()
    }

    pub(crate) fn native_set_start_location(&self, value: String) {
        *self
            .inner
            .start_location
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = value;
    }

    pub(crate) fn native_look_at(&self) -> Vector3 {
        *self
            .inner
            .look_at
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
    }

    pub(crate) fn native_set_look_at(&self, value: Vector3) {
        *self
            .inner
            .look_at
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = value;
        let _ = self
            .inner
            .movement
            .get()
            .map(|movement| movement.set_login_look_at(value));
    }

    pub(crate) fn native_home_region_handle(&self) -> u64 {
        *self
            .inner
            .home_region_handle
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
    }

    pub(crate) fn native_home_look_at(&self) -> Vector3 {
        *self
            .inner
            .home_look_at
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
    }

    pub(crate) fn native_local_id(&self) -> u32 {
        *self
            .inner
            .local_id
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
    }

    pub(crate) fn native_collision_plane(&self) -> Vector4 {
        *self
            .inner
            .collision_plane
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
    }

    pub(crate) fn native_health(&self) -> f32 {
        *self
            .inner
            .health
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
    }

    pub(crate) fn native_set_health(&self, value: f32) {
        *self
            .inner
            .health
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = value;
    }

    #[allow(clippy::too_many_arguments)]
    pub(crate) fn native_grab(
        &self,
        object_local_id: u32,
        grab_offset: Vector3,
        uv_coord: Vector3,
        st_coord: Vector3,
        face_index: i32,
        position: Vector3,
        normal: Vector3,
        binormal: Vector3,
    ) -> Result<(), Error> {
        let mut packet = crate::packets::ObjectGrabPacket::new_with_constructor()?;
        packet.agent_data.agent_id = self.native_agent_id();
        packet.agent_data.session_id = self.native_session_id();
        packet.object_data.local_id = object_local_id;
        packet.object_data.grab_offset = grab_offset;
        let mut surface = crate::packets::ObjectGrabPacketSurfaceInfoBlock::new_with_constructor()?;
        surface.uv_coord = uv_coord;
        surface.st_coord = st_coord;
        surface.face_index = face_index;
        surface.position = position;
        surface.normal = normal;
        surface.binormal = binormal;
        packet.surface_info = vec![surface];
        self.send_current_packet(
            crate::packets::PacketType::ObjectGrab,
            packet.to_bytes_with_method()?,
        )
    }

    #[allow(clippy::too_many_arguments)]
    pub(crate) fn native_grab_update(
        &self,
        object_id: UUID,
        grab_position: Vector3,
        grab_offset: Vector3,
        uv_coord: Vector3,
        st_coord: Vector3,
        face_index: i32,
        position: Vector3,
        normal: Vector3,
        binormal: Vector3,
    ) -> Result<(), Error> {
        let mut packet = crate::packets::ObjectGrabUpdatePacket::new_with_constructor()?;
        packet.agent_data.agent_id = self.native_agent_id();
        packet.agent_data.session_id = self.native_session_id();
        packet.object_data.object_id = object_id;
        packet.object_data.grab_offset_initial = grab_offset;
        packet.object_data.grab_position = grab_position;
        packet.object_data.time_since_last = 0;
        let mut surface =
            crate::packets::ObjectGrabUpdatePacketSurfaceInfoBlock::new_with_constructor()?;
        surface.uv_coord = uv_coord;
        surface.st_coord = st_coord;
        surface.face_index = face_index;
        surface.position = position;
        surface.normal = normal;
        surface.binormal = binormal;
        packet.surface_info = vec![surface];
        self.send_current_packet(
            crate::packets::PacketType::ObjectGrabUpdate,
            packet.to_bytes_with_method()?,
        )
    }

    #[allow(clippy::too_many_arguments)]
    pub(crate) fn native_de_grab(
        &self,
        object_local_id: u32,
        uv_coord: Vector3,
        st_coord: Vector3,
        face_index: i32,
        position: Vector3,
        normal: Vector3,
        binormal: Vector3,
    ) -> Result<(), Error> {
        let mut packet = crate::packets::ObjectDeGrabPacket::new_with_constructor()?;
        packet.agent_data.agent_id = self.native_agent_id();
        packet.agent_data.session_id = self.native_session_id();
        packet.object_data.local_id = object_local_id;
        let mut surface =
            crate::packets::ObjectDeGrabPacketSurfaceInfoBlock::new_with_constructor()?;
        surface.uv_coord = uv_coord;
        surface.st_coord = st_coord;
        surface.face_index = face_index;
        surface.position = position;
        surface.normal = normal;
        surface.binormal = binormal;
        packet.surface_info = vec![surface];
        self.send_current_packet(
            crate::packets::PacketType::ObjectDeGrab,
            packet.to_bytes_with_method()?,
        )
    }

    fn send_current_packet(
        &self,
        packet_type: crate::packets::PacketType,
        data: Vec<u8>,
    ) -> Result<(), Error> {
        let simulator = self
            .client
            .network()
            .native_current_sim()
            .ok_or(Error::InvalidOperation)?;
        send_simulator_packet(&simulator, packet_type, data)
    }

    pub(crate) fn native_set_height_width(&self, height: u16, width: u16) -> Result<(), Error> {
        let mut packet = crate::packets::AgentHeightWidthPacket::new_with_constructor()?;
        packet.agent_data.agent_id = self.native_agent_id();
        packet.agent_data.session_id = self.native_session_id();
        packet.agent_data.circuit_code = self.client.network().native_circuit_code();
        packet.height_width_block.height = height;
        packet.height_width_block.width = width;
        packet.height_width_block.gen_counter = self
            .inner
            .height_width_generation
            .fetch_add(1, Ordering::AcqRel);
        self.send_current_packet(
            crate::packets::PacketType::AgentHeightWidth,
            packet.to_bytes_with_method()?,
        )
    }

    pub(crate) fn native_set_home(&self) -> Result<(), Error> {
        let mut packet = crate::packets::SetStartLocationRequestPacket::new_with_constructor()?;
        packet.agent_data.agent_id = self.native_agent_id();
        packet.agent_data.session_id = self.native_session_id();
        packet.start_location_data.location_pos = self.native_movement_runtime().sim_position();
        packet.start_location_data.location_id = 1;
        packet.start_location_data.sim_name = Vec::new();
        packet.start_location_data.location_look_at =
            self.native_movement_runtime().camera_at_axis();
        self.send_current_packet(
            crate::packets::PacketType::SetStartLocationRequest,
            packet.to_bytes_with_method()?,
        )
    }

    pub(crate) fn native_script_question_reply(
        &self,
        simulator: Simulator,
        item_id: UUID,
        task_id: UUID,
        permissions: crate::ScriptPermission,
    ) -> Result<(), Error> {
        let mut packet = crate::packets::ScriptAnswerYesPacket::new_with_constructor()?;
        packet.agent_data.agent_id = self.native_agent_id();
        packet.agent_data.session_id = self.native_session_id();
        packet.data.item_id = item_id;
        packet.data.task_id = task_id;
        packet.data.questions = permissions.0;
        send_simulator_packet(
            &simulator,
            crate::packets::PacketType::ScriptAnswerYes,
            packet.to_bytes_with_method()?,
        )
    }

    pub(crate) fn native_request_script_sensor(
        &self,
        name: String,
        search_id: UUID,
        type_: crate::ScriptSensorTypeFlags,
        range: f32,
        arc: f32,
        request_id: UUID,
        simulator: Simulator,
    ) -> Result<(), Error> {
        let mut packet = crate::packets::ScriptSensorRequestPacket::new_with_constructor()?;
        packet.requester.arc = arc;
        packet.requester.range = range;
        packet.requester.region_handle = simulator.handle;
        packet.requester.request_id = request_id;
        packet.requester.search_dir = libremetaverse_types::Quaternion::identity();
        packet.requester.search_id = search_id;
        packet.requester.search_name = name.into_bytes();
        packet.requester.search_pos = Vector3::zero();
        packet.requester.search_regions = 0;
        packet.requester.source_id = self.native_agent_id();
        packet.requester.type_ = type_.0;
        send_simulator_packet(
            &simulator,
            crate::packets::PacketType::ScriptSensorRequest,
            packet.to_bytes_with_method()?,
        )
    }

    pub(crate) fn native_last_name(&self) -> String {
        self.inner
            .last_name
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .clone()
    }

    pub(crate) fn native_active_group(&self) -> UUID {
        *self
            .inner
            .active_group
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
    }

    pub(crate) fn native_set_active_group(&self, value: UUID) {
        *self
            .inner
            .active_group
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = value;
    }

    pub(crate) fn native_active_group_powers(&self) -> crate::GroupPowers {
        *self
            .inner
            .active_group_powers
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
    }

    pub(crate) fn native_set_active_group_powers(&self, value: crate::GroupPowers) {
        *self
            .inner
            .active_group_powers
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = value;
    }

    pub(crate) fn native_set_last_name(&self, value: String) {
        *self
            .inner
            .last_name
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = value;
    }

    pub(crate) fn native_agent_access(&self) -> String {
        self.inner
            .agent_access
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .clone()
    }

    pub(crate) fn native_set_agent_access(&self, value: String) {
        *self
            .inner
            .agent_access
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = value;
    }

    pub(crate) fn native_hover_height(&self) -> f32 {
        *self
            .inner
            .hover_height
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
    }

    pub(crate) fn native_set_hover_height(&self, value: f32) {
        *self
            .inner
            .hover_height
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = value;
    }

    pub(crate) fn native_balance(&self) -> i32 {
        *self
            .inner
            .balance
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
    }

    pub(crate) fn native_set_balance(&self, value: i32) {
        *self
            .inner
            .balance
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = value;
    }

    pub(crate) fn native_agent_id(&self) -> UUID {
        let value = *self
            .inner
            .agent_id
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        if value == UUID::zero() {
            self.client.network().native_agent_id()
        } else {
            value
        }
    }

    pub(crate) fn native_set_agent_id(&self, value: UUID) {
        *self
            .inner
            .agent_id
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = value;
    }

    pub(crate) fn native_session_id(&self) -> UUID {
        let value = *self
            .inner
            .session_id
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        if value == UUID::zero() {
            self.client.network().native_session_id()
        } else {
            value
        }
    }

    pub(crate) fn native_set_session_id(&self, value: UUID) {
        *self
            .inner
            .session_id
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = value;
    }

    fn send_packet(
        &self,
        packet_type: crate::packets::PacketType,
        data: Vec<u8>,
    ) -> Result<(), Error> {
        let length = i32::try_from(data.len()).map_err(|_| Error::Argument)?;
        let simulator = self
            .client
            .network()
            .current_sim()
            .ok_or(Error::InvalidOperation)?;
        let do_zerocode = data
            .first()
            .is_some_and(|flags| flags & crate::Helpers::MSG_ZEROCODED != 0);
        simulator.native_send_packet_data(data, length, packet_type, do_zerocode)
    }

    pub(crate) fn native_chat(
        &self,
        message: String,
        channel: i32,
        type_: ChatType,
        split_large_messages: Option<bool>,
    ) -> Result<(), Error> {
        for (packet_type, data) in
            self.native_chat_payloads(message, channel, type_, split_large_messages)?
        {
            self.send_packet(packet_type, data)?;
        }
        Ok(())
    }

    fn native_chat_payloads(
        &self,
        message: String,
        channel: i32,
        type_: ChatType,
        split_large_messages: Option<bool>,
    ) -> Result<Vec<(crate::packets::PacketType, Vec<u8>)>, Error> {
        if channel < 0 {
            let message = split_utf8(&message, MAX_DIALOG_LABEL_BYTES, 1)
                .into_iter()
                .next()
                .unwrap_or_default();
            let mut packet = crate::packets::ScriptDialogReplyPacket::new_with_constructor()?;
            packet.agent_data.agent_id = self.native_agent_id();
            packet.agent_data.session_id = self.native_session_id();
            packet.data.object_id = self.native_agent_id();
            packet.data.chat_channel = channel;
            packet.data.button_index = 0;
            packet.data.button_label = message.into_bytes();
            return Ok(vec![(
                crate::packets::PacketType::ScriptDialogReply,
                packet.to_bytes_with_method()?,
            )]);
        }

        let max_parts = if split_large_messages.unwrap_or(true) {
            usize::MAX
        } else {
            1
        };
        split_utf8(&message, MAX_CHAT_BYTES, max_parts)
            .into_iter()
            .map(|message| {
                let mut packet = crate::packets::ChatFromViewerPacket::new_with_constructor()?;
                packet.agent_data.agent_id = self.native_agent_id();
                packet.agent_data.session_id = self.native_session_id();
                packet.chat_data.channel = channel;
                packet.chat_data.message = message.into_bytes();
                packet.chat_data.type_ = type_ as u8;
                Ok((
                    crate::packets::PacketType::ChatFromViewer,
                    packet.to_bytes_with_method()?,
                ))
            })
            .collect()
    }

    pub(crate) fn native_instant_message(
        &self,
        target: UUID,
        message: String,
        session_id: Option<UUID>,
    ) -> Result<(), Error> {
        let session_id = session_id.unwrap_or_else(|| {
            if self.native_agent_id() == target {
                self.native_agent_id()
            } else {
                UUID::bitxor(target, self.native_agent_id())
            }
        });
        self.native_instant_message_full(
            self.native_name(),
            target,
            message,
            session_id,
            InstantMessageDialog::MessageFromAgent,
            InstantMessageOnline::Offline,
            Vector3::zero(),
            UUID::zero(),
            Vec::new(),
        )
    }

    #[allow(clippy::too_many_arguments)]
    pub(crate) fn native_instant_message_full(
        &self,
        from_name: String,
        target: UUID,
        message: String,
        mut im_session_id: UUID,
        dialog: InstantMessageDialog,
        offline: InstantMessageOnline,
        _position: Vector3,
        region_id: UUID,
        binary_bucket: Vec<u8>,
    ) -> Result<(), Error> {
        // Match the reference's safety guard. Message text is intentionally not logged.
        if target == UUID::zero() {
            return Ok(());
        }
        if im_session_id == UUID::zero() || im_session_id == self.native_agent_id() {
            im_session_id = if self.native_agent_id() == target {
                self.native_agent_id()
            } else {
                UUID::bitxor(target, self.native_agent_id())
            };
        }
        for message in split_utf8(&message, MAX_CHAT_BYTES, usize::MAX) {
            let mut packet = crate::packets::ImprovedInstantMessagePacket::new_with_constructor()?;
            packet.agent_data.agent_id = self.native_agent_id();
            packet.agent_data.session_id = self.native_session_id();
            packet.message_block.dialog = dialog as u8;
            packet.message_block.from_agent_name = from_name.as_bytes().to_vec();
            packet.message_block.from_group = false;
            packet.message_block.id = im_session_id;
            packet.message_block.message = message.into_bytes();
            packet.message_block.offline = offline as u8;
            packet.message_block.to_agent_id = target;
            packet
                .message_block
                .binary_bucket
                .clone_from(&binary_bucket);
            // The C# implementation ignores its position argument and sends zero.
            packet.message_block.position = Vector3::zero();
            packet.message_block.region_id = region_id;
            self.send_packet(
                crate::packets::PacketType::ImprovedInstantMessage,
                packet.to_bytes_with_method()?,
            )?;
        }
        Ok(())
    }

    pub(crate) fn native_instant_message_conference(
        &self,
        from_name: String,
        target: UUID,
        message: String,
        session_id: UUID,
        conference_ids: Vec<UUID>,
    ) -> Result<(), Error> {
        let mut bucket = Vec::with_capacity(conference_ids.len().saturating_mul(16));
        for id in conference_ids {
            bucket.extend_from_slice(&id.get_bytes()?);
        }
        self.native_instant_message_full(
            from_name,
            target,
            message,
            session_id,
            InstantMessageDialog::MessageFromAgent,
            InstantMessageOnline::Offline,
            Vector3::zero(),
            UUID::zero(),
            bucket,
        )
    }

    pub(crate) fn native_instant_message_group(
        &self,
        from_name: Option<String>,
        group_id: UUID,
        message: String,
    ) -> Result<(), Error> {
        if !self
            .group_chat_sessions
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .contains_key(&group_id)
        {
            return Ok(());
        }
        let from_name = from_name.unwrap_or_else(|| self.native_name());
        self.native_instant_message_full(
            from_name,
            group_id,
            message,
            group_id,
            InstantMessageDialog::SessionSend,
            InstantMessageOnline::Online,
            Vector3::zero(),
            UUID::zero(),
            vec![0],
        )
    }

    pub(crate) fn native_is_group_message(&self, message: InstantMessage) -> bool {
        message.group_im
            || self
                .group_chat_sessions
                .read()
                .unwrap_or_else(std::sync::PoisonError::into_inner)
                .contains_key(&message.im_session_id)
    }

    pub(crate) fn native_group_invite_respond(
        &self,
        group_id: UUID,
        im_session_id: UUID,
        accept: bool,
    ) -> Result<(), Error> {
        self.native_instant_message_full(
            self.native_name(),
            group_id,
            String::new(),
            im_session_id,
            if accept {
                InstantMessageDialog::GroupInvitationAccept
            } else {
                InstantMessageDialog::GroupInvitationDecline
            },
            InstantMessageOnline::Offline,
            Vector3::zero(),
            UUID::zero(),
            Vec::new(),
        )
    }

    pub(crate) fn native_join_group_chat(&self, group_id: UUID) -> Result<(), Error> {
        self.send_session_control(group_id, InstantMessageDialog::SessionGroupStart)
    }

    pub(crate) fn native_leave_group_chat(&self, group_id: UUID) -> Result<(), Error> {
        self.send_session_control(group_id, InstantMessageDialog::SessionDrop)?;
        self.group_chat_sessions
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .remove(&group_id);
        Ok(())
    }

    fn send_session_control(
        &self,
        group_id: UUID,
        dialog: InstantMessageDialog,
    ) -> Result<(), Error> {
        let mut packet = crate::packets::ImprovedInstantMessagePacket::new_with_constructor()?;
        packet.agent_data.agent_id = self.native_agent_id();
        packet.agent_data.session_id = self.native_session_id();
        packet.message_block.dialog = dialog as u8;
        packet.message_block.from_agent_name = self.native_name().into_bytes();
        packet.message_block.id = group_id;
        packet.message_block.to_agent_id = group_id;
        packet.message_block.position = Vector3::zero();
        self.send_packet(
            crate::packets::PacketType::ImprovedInstantMessage,
            packet.to_bytes_with_method()?,
        )
    }

    pub(crate) fn native_reply_to_script_dialog(
        &self,
        channel: i32,
        button_index: i32,
        button_label: String,
        object_id: UUID,
    ) -> Result<(), Error> {
        let mut packet = crate::packets::ScriptDialogReplyPacket::new_with_constructor()?;
        packet.agent_data.agent_id = self.native_agent_id();
        packet.agent_data.session_id = self.native_session_id();
        packet.data.chat_channel = channel;
        packet.data.button_index = button_index;
        packet.data.button_label = button_label.into_bytes();
        packet.data.object_id = object_id;
        self.send_packet(
            crate::packets::PacketType::ScriptDialogReply,
            packet.to_bytes_with_method()?,
        )
    }

    pub(crate) fn native_activate_gesture(
        &self,
        inventory_id: UUID,
        asset_id: UUID,
    ) -> Result<(), Error> {
        let mut packet = crate::packets::ActivateGesturesPacket::new_with_constructor()?;
        packet.agent_data.agent_id = self.native_agent_id();
        packet.agent_data.session_id = self.native_session_id();
        let mut block = crate::packets::ActivateGesturesPacketDataBlock::new_with_constructor()?;
        block.item_id = inventory_id;
        block.asset_id = asset_id;
        packet.data = vec![block];
        self.send_packet(
            crate::packets::PacketType::ActivateGestures,
            packet.to_bytes_with_method()?,
        )?;
        self.inner
            .active_gestures
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .insert(inventory_id, asset_id);
        Ok(())
    }

    pub(crate) fn native_deactivate_gesture(&self, inventory_id: UUID) -> Result<(), Error> {
        let mut packet = crate::packets::DeactivateGesturesPacket::new_with_constructor()?;
        packet.agent_data.agent_id = self.native_agent_id();
        packet.agent_data.session_id = self.native_session_id();
        let mut block = crate::packets::DeactivateGesturesPacketDataBlock::new_with_constructor()?;
        block.item_id = inventory_id;
        packet.data = vec![block];
        self.send_packet(
            crate::packets::PacketType::DeactivateGestures,
            packet.to_bytes_with_method()?,
        )?;
        self.inner
            .active_gestures
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .remove(&inventory_id);
        Ok(())
    }

    pub(crate) async fn native_play_gesture(
        &self,
        gesture_id: UUID,
        cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
    ) -> Result<(), Error> {
        let cancellation = cancellation_token.unwrap_or_else(|| self.client.cancellation_token());
        cancellation.throw_if_cancellation_requested()?;
        tokio::select! {
            result = self.play_gesture_inner(gesture_id, cancellation.clone()) => result,
            () = cancellation.cancelled() => Err(Error::Cancelled),
            () = tokio::time::sleep(GESTURE_TIMEOUT) => Err(Error::Cancelled),
        }
    }

    async fn play_gesture_inner(
        &self,
        gesture_id: UUID,
        cancellation: libremetaverse_types::compat::CancellationToken,
    ) -> Result<(), Error> {
        let Some(data) = self
            .fetch_gesture_asset(gesture_id, cancellation.clone())
            .await?
        else {
            return Ok(());
        };
        let mut gesture = crate::assets::AssetGesture::new_with_uuid_bytes(gesture_id, data)?;
        if !gesture.decode()? {
            return Ok(());
        }
        for step in gesture.sequence {
            cancellation.throw_if_cancellation_requested()?;
            match step {
                crate::assets::GestureStep::Chat(step) => {
                    let (channel, text) = gesture_chat_channel(&step.text);
                    self.native_chat(text, channel, ChatType::Normal, None)?;
                }
                crate::assets::GestureStep::Animation(step) => {
                    if step.animation_start {
                        if self
                            .signaled_animations
                            .read()
                            .unwrap_or_else(std::sync::PoisonError::into_inner)
                            .contains_key(&step.id)
                        {
                            self.native_animation_stop(step.id, true)?;
                        }
                        self.native_animation_start(step.id, true)?;
                    } else {
                        self.native_animation_stop(step.id, true)?;
                    }
                }
                crate::assets::GestureStep::Sound(step) => {
                    self.native_play_gesture_sound(step.id)?;
                }
                crate::assets::GestureStep::Wait(step) => {
                    if step.wait_for_time {
                        let seconds = f64::from(step.wait_time.max(0.0));
                        tokio::select! {
                            () = tokio::time::sleep(Duration::from_secs_f64(seconds)) => {},
                            () = cancellation.cancelled() => return Err(Error::Cancelled),
                        }
                    }
                    // Preserve the reference behavior: WaitForAnimation is
                    // recognized but intentionally has no blocking behavior.
                }
                crate::assets::GestureStep::EOF(_) => break,
            }
        }
        Ok(())
    }

    async fn fetch_gesture_asset(
        &self,
        gesture_id: UUID,
        cancellation: libremetaverse_types::compat::CancellationToken,
    ) -> Result<Option<Vec<u8>>, Error> {
        let simulator = self
            .client
            .network()
            .current_sim()
            .ok_or(Error::InvalidOperation)?;
        if let Some(viewer_asset) = simulator
            .native_caps()
            .and_then(|caps| caps.capability_uri("ViewerAsset".into()).ok().flatten())
        {
            let separator = if viewer_asset.0.contains('?') {
                '&'
            } else {
                '?'
            };
            let uri = Uri(format!(
                "{}{separator}gesture_id={gesture_id}",
                viewer_asset.0
            ));
            let response = self
                .client
                .http_caps_client()
                .get(uri, cancellation, None)
                .await;
            return match response {
                Ok((response, data)) if response.is_success_status_code() => Ok(Some(data)),
                Err(Error::Cancelled) => Err(Error::Cancelled),
                Ok(_) | Err(_) => Ok(None),
            };
        }
        self.fetch_gesture_asset_udp(simulator, gesture_id, cancellation)
            .await
    }

    async fn fetch_gesture_asset_udp(
        &self,
        simulator: Simulator,
        gesture_id: UUID,
        cancellation: libremetaverse_types::compat::CancellationToken,
    ) -> Result<Option<Vec<u8>>, Error> {
        let transfer_id = UUID::random()?;
        let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
        let _subscription = self
            .client
            .network()
            .subscribe_raw_packet(Arc::new(move |event| {
                if matches!(
                    event.packet_type,
                    crate::packets::PacketType::TransferInfo
                        | crate::packets::PacketType::TransferPacket
                ) {
                    let _ = sender.send(event);
                }
            }));
        let mut request = crate::packets::TransferRequestPacket::new_with_constructor()?;
        request.transfer_info.channel_type = crate::ChannelType::Asset as i32;
        request.transfer_info.priority = 101.0;
        request.transfer_info.source_type = crate::SourceType::Asset as i32;
        request.transfer_info.transfer_id = transfer_id;
        request.transfer_info.params = gesture_id.get_bytes()?;
        request
            .transfer_info
            .params
            .extend_from_slice(&(libremetaverse_types::AssetType::Gesture as i32).to_le_bytes());
        let data = request.to_bytes_with_method()?;
        let length = i32::try_from(data.len()).map_err(|_| Error::Argument)?;
        simulator.native_send_packet_data(
            data,
            length,
            crate::packets::PacketType::TransferRequest,
            false,
        )?;
        let mut abort = UdpAssetAbortGuard::new(simulator, transfer_id);
        let mut size = None;
        let mut chunks = BTreeMap::<i32, Vec<u8>>::new();
        loop {
            let event = tokio::select! {
                event = receiver.recv() => event.ok_or(Error::InvalidOperation)?,
                () = cancellation.cancelled() => return Err(Error::Cancelled),
            };
            match event.packet_type {
                crate::packets::PacketType::TransferInfo => {
                    let packet =
                        decode_raw_packet::<crate::packets::TransferInfoPacket>(&event.data)?;
                    if packet.transfer_info.transfer_id != transfer_id {
                        continue;
                    }
                    if packet.transfer_info.status != crate::StatusCode::OK as i32 {
                        abort.complete();
                        return Ok(None);
                    }
                    let announced =
                        usize::try_from(packet.transfer_info.size).map_err(|_| Error::Argument)?;
                    if announced > MAX_GESTURE_ASSET_BYTES {
                        return Err(Error::Argument);
                    }
                    size = Some(announced);
                }
                crate::packets::PacketType::TransferPacket => {
                    let packet =
                        decode_raw_packet::<crate::packets::TransferPacketPacket>(&event.data)?;
                    if packet.transfer_data.transfer_id != transfer_id
                        || packet.transfer_data.packet < 0
                    {
                        continue;
                    }
                    chunks
                        .entry(packet.transfer_data.packet)
                        .or_insert(packet.transfer_data.data);
                }
                _ => continue,
            }
            let Some(expected_size) = size else {
                continue;
            };
            let mut assembled = Vec::with_capacity(expected_size);
            for packet_number in 0_i32.. {
                let Some(chunk) = chunks.get(&packet_number) else {
                    break;
                };
                let remaining = expected_size.saturating_sub(assembled.len());
                assembled.extend_from_slice(&chunk[..chunk.len().min(remaining)]);
                if assembled.len() == expected_size {
                    abort.complete();
                    return Ok(Some(assembled));
                }
            }
            if expected_size == 0 {
                abort.complete();
                return Ok(Some(Vec::new()));
            }
        }
    }

    fn native_play_gesture_sound(&self, sound_id: UUID) -> Result<(), Error> {
        if sound_id == UUID::zero() {
            return Ok(());
        }
        let simulator = self
            .client
            .network()
            .current_sim()
            .ok_or(Error::InvalidOperation)?;
        let mut packet = crate::packets::SoundTriggerPacket::new_with_constructor()?;
        packet.sound_data.sound_id = sound_id;
        packet.sound_data.handle = simulator.handle;
        packet.sound_data.position = Vector3::zero();
        packet.sound_data.gain = 1.0;
        self.send_packet(
            crate::packets::PacketType::SoundTrigger,
            packet.to_bytes_with_method()?,
        )
    }

    pub(crate) fn native_active_gestures(&self) -> RwLock<HashMap<UUID, UUID>> {
        RwLock::new(
            self.inner
                .active_gestures
                .read()
                .unwrap_or_else(std::sync::PoisonError::into_inner)
                .clone(),
        )
    }

    pub(crate) fn native_request_balance(&self) -> Result<(), Error> {
        let mut packet = crate::packets::MoneyBalanceRequestPacket::new_with_constructor()?;
        packet.agent_data.agent_id = self.native_agent_id();
        packet.agent_data.session_id = self.native_session_id();
        packet.money_data.transaction_id = UUID::zero();
        self.send_packet(
            crate::packets::PacketType::MoneyBalanceRequest,
            packet.to_bytes_with_method()?,
        )
    }

    pub(crate) fn native_animation_start(
        &self,
        animation: UUID,
        reliable: bool,
    ) -> Result<(), Error> {
        self.native_animate(HashMap::from([(animation, true)]), reliable)
    }

    pub(crate) fn native_animation_stop(
        &self,
        animation: UUID,
        reliable: bool,
    ) -> Result<(), Error> {
        self.native_animate(HashMap::from([(animation, false)]), reliable)
    }

    pub(crate) fn native_animate(
        &self,
        animations: HashMap<UUID, bool>,
        reliable: bool,
    ) -> Result<(), Error> {
        let mut packet = crate::packets::AgentAnimationPacket::new_with_constructor()?;
        packet.agent_data.agent_id = self.native_agent_id();
        packet.agent_data.session_id = self.native_session_id();
        packet.animation_list = animations
            .iter()
            .map(|(animation, start)| {
                let mut block =
                    crate::packets::AgentAnimationPacketAnimationListBlock::new_with_constructor()?;
                block.anim_id = *animation;
                block.start_anim = *start;
                Ok(block)
            })
            .collect::<Result<Vec<_>, Error>>()?;
        packet.physical_avatar_event_list = Vec::new();
        let mut data = packet.to_bytes_with_method()?;
        let flags = data.first_mut().ok_or(Error::Argument)?;
        if reliable {
            *flags |= crate::Helpers::MSG_RELIABLE;
        } else {
            *flags &= !crate::Helpers::MSG_RELIABLE;
        }
        self.send_packet(crate::packets::PacketType::AgentAnimation, data)?;
        let mut signaled = self
            .signaled_animations
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        for (animation, start) in animations {
            if start {
                signaled.insert(animation, 1);
            } else {
                signaled.remove(&animation);
            }
        }
        Ok(())
    }

    pub(crate) fn native_request_mute_list(&self) -> Result<(), Error> {
        let mut packet = crate::packets::MuteListRequestPacket::new_with_constructor()?;
        packet.agent_data.agent_id = self.native_agent_id();
        packet.agent_data.session_id = self.native_session_id();
        packet.mute_data.mute_crc = 0;
        self.send_packet(
            crate::packets::PacketType::MuteListRequest,
            packet.to_bytes_with_method()?,
        )
    }

    pub(crate) fn native_update_mute_list_entry(
        &self,
        type_: crate::MuteType,
        id: UUID,
        name: String,
        flags: crate::MuteFlags,
    ) -> Result<(), Error> {
        let mut packet = crate::packets::UpdateMuteListEntryPacket::new_with_constructor()?;
        packet.agent_data.agent_id = self.native_agent_id();
        packet.agent_data.session_id = self.native_session_id();
        packet.mute_data.mute_type = type_ as i32;
        packet.mute_data.mute_id = id;
        packet.mute_data.mute_name = name.as_bytes().to_vec();
        packet.mute_data.mute_flags = u32::try_from(flags.0).map_err(|_| Error::Argument)?;
        self.send_packet(
            crate::packets::PacketType::UpdateMuteListEntry,
            packet.to_bytes_with_method()?,
        )?;
        self.mute_list
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .insert(
                format!("{id}|{name}"),
                MuteEntry {
                    flags,
                    id,
                    name,
                    type_,
                },
            );
        self.inner.mute_list_updated.emit(());
        Ok(())
    }

    pub(crate) fn native_remove_mute_list_entry(
        &self,
        id: UUID,
        name: String,
    ) -> Result<(), Error> {
        let mut packet = crate::packets::RemoveMuteListEntryPacket::new_with_constructor()?;
        packet.agent_data.agent_id = self.native_agent_id();
        packet.agent_data.session_id = self.native_session_id();
        packet.mute_data.mute_id = id;
        packet.mute_data.mute_name = name.as_bytes().to_vec();
        self.send_packet(
            crate::packets::PacketType::RemoveMuteListEntry,
            packet.to_bytes_with_method()?,
        )?;
        if self
            .mute_list
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .remove(&format!("{id}|{name}"))
            .is_some()
        {
            self.inner.mute_list_updated.emit(());
        }
        Ok(())
    }

    pub(crate) fn native_give_money(
        &self,
        target: UUID,
        amount: i32,
        description: String,
        type_: MoneyTransactionType,
        flags: TransactionFlags,
    ) -> Result<(), Error> {
        let mut packet = crate::packets::MoneyTransferRequestPacket::new_with_constructor()?;
        packet.agent_data.agent_id = self.native_agent_id();
        packet.agent_data.session_id = self.native_session_id();
        packet.money_data.description = description.into_bytes();
        packet.money_data.dest_id = target;
        packet.money_data.source_id = self.native_agent_id();
        packet.money_data.transaction_type = type_ as i32;
        packet.money_data.flags = flags.0;
        packet.money_data.amount = amount;
        self.send_packet(
            crate::packets::PacketType::MoneyTransferRequest,
            packet.to_bytes_with_method()?,
        )
    }

    pub(crate) fn native_give_avatar_money(
        &self,
        target: UUID,
        amount: i32,
        description: String,
    ) -> Result<(), Error> {
        self.native_give_money(
            target,
            amount,
            description,
            MoneyTransactionType::Gift,
            TransactionFlags::NONE,
        )
    }

    pub(crate) fn native_give_group_money(
        &self,
        target: UUID,
        amount: i32,
        description: String,
    ) -> Result<(), Error> {
        self.native_give_money(
            target,
            amount,
            description,
            MoneyTransactionType::Gift,
            TransactionFlags::DEST_GROUP,
        )
    }

    pub(crate) fn native_give_object_money(
        &self,
        target: UUID,
        amount: i32,
        object_name: String,
    ) -> Result<(), Error> {
        self.native_give_money(
            target,
            amount,
            object_name,
            MoneyTransactionType::PayObject,
            TransactionFlags::NONE,
        )
    }

    pub(crate) fn native_pay_upload_fee(
        &self,
        amount: Option<i32>,
        description: String,
    ) -> Result<(), Error> {
        let benefit_cost = self.native_viewer_benefits().map_or(0, |benefits| {
            benefits.account_level_benefits.texture_upload_cost
        });
        self.native_give_money(
            UUID::zero(),
            amount.unwrap_or_else(|| {
                if benefit_cost > 0 {
                    benefit_cost
                } else {
                    self.client.settings_ref().upload_cost
                }
            }),
            description,
            MoneyTransactionType::UploadCharge,
            TransactionFlags::NONE,
        )
    }

    pub(crate) fn native_viewer_effect(
        &self,
        source_avatar: UUID,
        target_object: UUID,
        global_offset: Vector3d,
        color: Color4,
        duration: f32,
        effect_id: UUID,
        effect_type: EffectType,
        subtype: Option<u8>,
    ) -> Result<(), Error> {
        let mut type_data = Vec::with_capacity(if subtype.is_some() { 57 } else { 56 });
        type_data.extend_from_slice(&source_avatar.get_bytes()?);
        type_data.extend_from_slice(&target_object.get_bytes()?);
        type_data.extend_from_slice(&global_offset.get_bytes()?);
        if let Some(subtype) = subtype {
            type_data.push(subtype);
        }
        let mut packet = crate::packets::ViewerEffectPacket::new_with_constructor()?;
        packet.agent_data.agent_id = self.native_agent_id();
        packet.agent_data.session_id = self.native_session_id();
        let mut effect = crate::packets::ViewerEffectPacketEffectBlock::new_with_constructor()?;
        effect.agent_id = self.native_agent_id();
        effect.color = color.get_bytes_with_method()?;
        effect.duration = duration;
        effect.id = effect_id;
        effect.type_ = effect_type as u8;
        effect.type_data = type_data;
        packet.effect = vec![effect];
        self.send_packet(
            crate::packets::PacketType::ViewerEffect,
            packet.to_bytes_with_method()?,
        )
    }

    pub(crate) fn native_point_at_effect(
        &self,
        source_avatar: UUID,
        target_object: UUID,
        global_offset: Vector3d,
        type_: PointAtType,
        effect_id: UUID,
    ) -> Result<(), Error> {
        let duration = if type_ == PointAtType::Clear {
            0.0
        } else {
            f32::MAX / 4.0
        };
        self.native_viewer_effect(
            source_avatar,
            target_object,
            global_offset,
            Color4::default(),
            duration,
            effect_id,
            EffectType::PointAt,
            Some(type_ as u8),
        )
    }

    pub(crate) fn native_look_at_effect(
        &self,
        source_avatar: UUID,
        target_object: UUID,
        global_offset: Vector3d,
        type_: LookAtType,
        effect_id: UUID,
    ) -> Result<(), Error> {
        let duration = match type_ {
            LookAtType::Clear | LookAtType::FreeLook => 2.0,
            LookAtType::Hover => 1.0,
            LookAtType::Idle => 3.0,
            LookAtType::AutoListen | LookAtType::Respond => 4.0,
            LookAtType::None | LookAtType::Select | LookAtType::Focus | LookAtType::Mouselook => {
                f32::MAX / 2.0
            }
            LookAtType::Conversation => 0.0,
        };
        self.native_viewer_effect(
            source_avatar,
            target_object,
            global_offset,
            Color4::default(),
            duration,
            effect_id,
            EffectType::LookAt,
            Some(type_ as u8),
        )
    }

    pub(crate) fn native_name(&self) -> String {
        let first = self.native_first_name();
        let last = self.native_last_name();
        match (first.is_empty(), last.is_empty()) {
            (false, false) => format!("{first} {last}"),
            (false, true) => first,
            (true, false) => last,
            (true, true) => String::new(),
        }
    }

    pub(crate) fn native_update_profile_udp(
        &self,
        profile: crate::AvatarAvatarProperties,
    ) -> Result<(), Error> {
        let mut packet = crate::packets::AvatarPropertiesUpdatePacket::new_with_constructor()?;
        packet.agent_data.agent_id = self.native_agent_id();
        packet.agent_data.session_id = self.native_session_id();
        packet.properties_data.about_text = profile.about_text.into_bytes();
        packet.properties_data.allow_publish =
            profile.flags.0 & crate::ProfileFlags::ALLOW_PUBLISH.0 != 0;
        packet.properties_data.fl_about_text = profile.first_life_text.into_bytes();
        packet.properties_data.fl_image_id = profile.first_life_image;
        packet.properties_data.image_id = profile.profile_image;
        packet.properties_data.mature_publish =
            profile.flags.0 & crate::ProfileFlags::MATURE_PUBLISH.0 != 0;
        packet.properties_data.profile_url = profile.profile_url.into_bytes();
        self.send_packet(
            crate::packets::PacketType::AvatarPropertiesUpdate,
            packet.to_bytes_with_method()?,
        )
    }

    fn profile_payload(profile: &crate::AvatarAvatarProperties) -> Result<OSDMap, Error> {
        OSDMap::new_with_dictionary(HashMap::from([
            (
                "sl_about_text".into(),
                OSD::String(profile.about_text.clone()),
            ),
            (
                "fl_about_text".into(),
                OSD::String(profile.first_life_text.clone()),
            ),
            (
                "profile_url".into(),
                OSD::String(profile.profile_url.clone()),
            ),
            (
                "allow_publish".into(),
                OSD::Boolean(profile.flags.0 & crate::ProfileFlags::ALLOW_PUBLISH.0 != 0),
            ),
            ("sl_image_id".into(), OSD::UUID(profile.profile_image)),
            ("fl_image_id".into(), OSD::UUID(profile.first_life_image)),
        ]))
    }

    async fn put_agent_profile(
        &self,
        target: UUID,
        payload: OSDMap,
        cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
    ) -> Result<(), Error> {
        let cancellation = cancellation_token.unwrap_or_else(|| self.client.cancellation_token());
        cancellation.throw_if_cancellation_requested()?;
        let Some(capability) = self.native_capability("AgentProfile", &cancellation)? else {
            return Ok(());
        };
        let uri = Uri(format!("{}/{}", capability.0.trim_end_matches('/'), target));
        let (response, _) = self
            .client
            .http_caps_client()
            .put_with_uri_osd_format_osd_cancellation_token_i_progress(
                uri,
                libremetaverse_structured_data::OSDFormat::Xml,
                OSD::Map(payload.snapshot()),
                cancellation,
                None,
            )
            .await?;
        if response.is_success_status_code() {
            Ok(())
        } else {
            Err(Error::InvalidOperation)
        }
    }

    pub(crate) async fn native_update_profile_async(
        &self,
        profile: crate::AvatarAvatarProperties,
        cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
    ) -> Result<(), Error> {
        self.put_agent_profile(
            self.native_agent_id(),
            Self::profile_payload(&profile)?,
            cancellation_token,
        )
        .await
    }

    pub(crate) fn native_update_profile(
        &self,
        profile: crate::AvatarAvatarProperties,
    ) -> Result<(), Error> {
        let capability_available = self
            .client
            .network()
            .current_sim()
            .and_then(|simulator| simulator.native_caps())
            .and_then(|caps| caps.capability_uri("AgentProfile".into()).ok().flatten())
            .is_some();
        if !capability_available {
            return self.native_update_profile_udp(profile);
        }
        let client = Arc::clone(&self.client);
        let target = self.native_agent_id();
        let payload = Self::profile_payload(&profile)?;
        std::thread::Builder::new()
            .name("libremetaverse-profile-update".into())
            .spawn(move || {
                let Ok(runtime) = tokio::runtime::Builder::new_current_thread()
                    .enable_all()
                    .build()
                else {
                    return;
                };
                let Ok(manager) = AgentManager::native_new(Some(client)) else {
                    return;
                };
                let _ = runtime.block_on(manager.put_agent_profile(target, payload, None));
            })
            .map(|_| ())
            .map_err(|_| Error::InvalidOperation)
    }

    fn native_update_profile_notes_udp(&self, target: UUID, notes: String) -> Result<(), Error> {
        let mut packet = crate::packets::AvatarNotesUpdatePacket::new_with_constructor()?;
        packet.agent_data.agent_id = self.native_agent_id();
        packet.agent_data.session_id = self.native_session_id();
        packet.data.target_id = target;
        packet.data.notes = notes.into_bytes();
        self.send_packet(
            crate::packets::PacketType::AvatarNotesUpdate,
            packet.to_bytes_with_method()?,
        )
    }

    pub(crate) async fn native_update_profile_notes_async(
        &self,
        target: UUID,
        notes: String,
        cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
    ) -> Result<(), Error> {
        self.put_agent_profile(
            target,
            OSDMap::new_with_dictionary(HashMap::from([("notes".into(), OSD::String(notes))]))?,
            cancellation_token,
        )
        .await
    }

    pub(crate) fn native_update_profile_notes(
        &self,
        target: UUID,
        notes: String,
    ) -> Result<(), Error> {
        let capability_available = self
            .client
            .network()
            .current_sim()
            .and_then(|simulator| simulator.native_caps())
            .and_then(|caps| caps.capability_uri("AgentProfile".into()).ok().flatten())
            .is_some();
        if !capability_available {
            return self.native_update_profile_notes_udp(target, notes);
        }
        let client = Arc::clone(&self.client);
        let payload =
            OSDMap::new_with_dictionary(HashMap::from([("notes".into(), OSD::String(notes))]))?;
        std::thread::Builder::new()
            .name("libremetaverse-profile-notes-update".into())
            .spawn(move || {
                let Ok(runtime) = tokio::runtime::Builder::new_current_thread()
                    .enable_all()
                    .build()
                else {
                    return;
                };
                let Ok(manager) = AgentManager::native_new(Some(client)) else {
                    return;
                };
                let _ = runtime.block_on(manager.put_agent_profile(target, payload, None));
            })
            .map(|_| ())
            .map_err(|_| Error::InvalidOperation)
    }

    pub(crate) fn native_update_interests(
        &self,
        interests: crate::AvatarInterests,
    ) -> Result<(), Error> {
        let mut packet = crate::packets::AvatarInterestsUpdatePacket::new_with_constructor()?;
        packet.agent_data.agent_id = self.native_agent_id();
        packet.agent_data.session_id = self.native_session_id();
        packet.properties_data.languages_text = interests.languages_text.into_bytes();
        packet.properties_data.skills_mask = interests.skills_mask;
        packet.properties_data.skills_text = interests.skills_text.into_bytes();
        packet.properties_data.want_to_mask = interests.want_to_mask;
        packet.properties_data.want_to_text = interests.want_to_text.into_bytes();
        self.send_packet(
            crate::packets::PacketType::AvatarInterestsUpdate,
            packet.to_bytes_with_method()?,
        )
    }

    #[allow(clippy::too_many_arguments)]
    pub(crate) fn native_pick_info_update(
        &self,
        pick_id: UUID,
        top_pick: bool,
        parcel_id: UUID,
        name: String,
        global_position: Vector3d,
        texture_id: UUID,
        description: String,
    ) -> Result<(), Error> {
        let mut packet = crate::packets::PickInfoUpdatePacket::new_with_constructor()?;
        packet.agent_data.agent_id = self.native_agent_id();
        packet.agent_data.session_id = self.native_session_id();
        packet.data.pick_id = pick_id;
        packet.data.desc = description.into_bytes();
        packet.data.creator_id = self.native_agent_id();
        packet.data.top_pick = top_pick;
        packet.data.parcel_id = parcel_id;
        packet.data.name = name.into_bytes();
        packet.data.snapshot_id = texture_id;
        packet.data.pos_global = global_position;
        packet.data.sort_order = 0;
        packet.data.enabled = false;
        self.send_packet(
            crate::packets::PacketType::PickInfoUpdate,
            packet.to_bytes_with_method()?,
        )
    }

    pub(crate) fn native_pick_delete(&self, pick_id: UUID) -> Result<(), Error> {
        let mut packet = crate::packets::PickDeletePacket::new_with_constructor()?;
        packet.agent_data.agent_id = self.native_agent_id();
        packet.agent_data.session_id = self.native_session_id();
        packet.data.pick_id = pick_id;
        self.send_packet(
            crate::packets::PacketType::PickDelete,
            packet.to_bytes_with_method()?,
        )
    }

    #[allow(clippy::too_many_arguments)]
    pub(crate) fn native_update_classified_info(
        &self,
        classified_id: UUID,
        category: crate::DirectoryManagerClassifiedCategories,
        snapshot_id: UUID,
        price: i32,
        position: Vector3d,
        name: String,
        description: String,
        auto_renew: bool,
    ) -> Result<(), Error> {
        let mut packet = crate::packets::ClassifiedInfoUpdatePacket::new_with_constructor()?;
        packet.agent_data.agent_id = self.native_agent_id();
        packet.agent_data.session_id = self.native_session_id();
        packet.data.classified_id = classified_id;
        packet.data.category = category as u32;
        packet.data.parcel_id = UUID::zero();
        packet.data.parent_estate = 0;
        packet.data.snapshot_id = snapshot_id;
        packet.data.pos_global = position;
        packet.data.classified_flags = if auto_renew { 32 } else { 0 };
        packet.data.price_for_listing = price;
        packet.data.name = name.into_bytes();
        packet.data.desc = description.into_bytes();
        self.send_packet(
            crate::packets::PacketType::ClassifiedInfoUpdate,
            packet.to_bytes_with_method()?,
        )
    }

    pub(crate) fn native_delete_classified(&self, classified_id: UUID) -> Result<(), Error> {
        let mut packet = crate::packets::ClassifiedDeletePacket::new_with_constructor()?;
        packet.agent_data.agent_id = self.native_agent_id();
        packet.agent_data.session_id = self.native_session_id();
        packet.data.classified_id = classified_id;
        self.send_packet(
            crate::packets::PacketType::ClassifiedDelete,
            packet.to_bytes_with_method()?,
        )
    }

    async fn get_map(
        &self,
        uri: Uri,
        cancellation: libremetaverse_types::compat::CancellationToken,
    ) -> Result<Option<OSDMap>, Error> {
        cancellation.throw_if_cancellation_requested()?;
        let response = self
            .client
            .http_caps_client()
            .get(uri, cancellation, None)
            .await;
        let (response, data) = match response {
            Ok(value) => value,
            Err(Error::Cancelled) => return Err(Error::Cancelled),
            Err(_) => return Ok(None),
        };
        if !response.is_success_status_code() {
            return Ok(None);
        }
        match OSDParser::deserialize_with_bytes(data) {
            Ok(OSD::Map(map)) => OSDMap::new_with_dictionary(map).map(Some),
            _ => Ok(None),
        }
    }

    async fn get_capability_map(
        &self,
        capability: &str,
        suffix: &str,
        cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
    ) -> Result<Option<OSDMap>, Error> {
        let cancellation = cancellation_token.unwrap_or_else(|| self.client.cancellation_token());
        let Some(uri) = self.native_capability(capability, &cancellation)? else {
            return Ok(None);
        };
        self.get_map(Uri(format!("{}{suffix}", uri.0)), cancellation)
            .await
    }

    async fn post_capability_map(
        &self,
        capability: &str,
        payload: OSDMap,
        cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
    ) -> Result<Option<OSDMap>, Error> {
        let cancellation = cancellation_token.unwrap_or_else(|| self.client.cancellation_token());
        let Some(uri) = self.native_capability(capability, &cancellation)? else {
            return Ok(None);
        };
        let response = self
            .client
            .http_caps_client()
            .post_with_uri_osd_format_osd_cancellation_token_i_progress(
                uri,
                libremetaverse_structured_data::OSDFormat::Xml,
                OSD::Map(payload.snapshot()),
                cancellation,
                None,
            )
            .await;
        let (response, data) = match response {
            Ok(value) => value,
            Err(Error::Cancelled) => return Err(Error::Cancelled),
            Err(_) => return Ok(None),
        };
        if !response.is_success_status_code() {
            return Ok(None);
        }
        if data.is_empty() {
            return OSDMap::new_with_dictionary(HashMap::new()).map(Some);
        }
        match OSDParser::deserialize_with_bytes(data) {
            Ok(OSD::Map(map)) => OSDMap::new_with_dictionary(map).map(Some),
            _ => Ok(None),
        }
    }

    async fn get_experience_list(
        &self,
        capability: &str,
        suffix: &str,
        cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
    ) -> Result<Option<ExperienceListMessage>, Error> {
        let Some(map) = self
            .get_capability_map(capability, suffix, cancellation_token)
            .await?
        else {
            return Ok(None);
        };
        let mut result = ExperienceListMessage::new()?;
        if result.deserialize(map).is_err() {
            return Ok(None);
        }
        Ok(Some(result))
    }

    pub(crate) async fn native_get_agent_experiences(
        &self,
        cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
    ) -> Result<Option<ExperienceListMessage>, Error> {
        let result = self
            .get_experience_list("AgentExperiences", "", cancellation_token)
            .await?;
        if let Some(message) = result.clone() {
            self.native_set_agent_experience_list(Some(message.clone()));
            self.inner
                .agent_experiences_updated
                .emit(AgentExperiencesEventArgs::new(message)?);
        }
        Ok(result)
    }

    pub(crate) async fn native_get_admin_experiences(
        &self,
        cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
    ) -> Result<Option<ExperienceListMessage>, Error> {
        self.get_experience_list("GetAdminExperiences", "", cancellation_token)
            .await
    }

    pub(crate) async fn native_get_creator_experiences(
        &self,
        cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
    ) -> Result<Option<ExperienceListMessage>, Error> {
        self.get_experience_list("GetCreatorExperiences", "", cancellation_token)
            .await
    }

    pub(crate) async fn native_get_group_experiences(
        &self,
        group_id: UUID,
        cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
    ) -> Result<Option<ExperienceListMessage>, Error> {
        self.get_experience_list(
            "GroupExperiences",
            &format!("?{group_id}"),
            cancellation_token,
        )
        .await
    }

    async fn get_experience_preferences_for_capability(
        &self,
        capability: &str,
        suffix: &str,
        cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
    ) -> Result<Option<ExperiencePreferencesMessage>, Error> {
        let Some(map) = self
            .get_capability_map(capability, suffix, cancellation_token)
            .await?
        else {
            return Ok(None);
        };
        let mut result = ExperiencePreferencesMessage::new()?;
        if result.deserialize(map).is_err() {
            return Ok(None);
        }
        self.native_set_experience_preferences(Some(result.clone()));
        self.inner
            .experience_preferences_updated
            .emit(ExperiencePreferencesEventArgs::new(result.clone())?);
        Ok(Some(result))
    }

    pub(crate) async fn native_get_agent_experience_permissions(
        &self,
        cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
    ) -> Result<Option<ExperiencePreferencesMessage>, Error> {
        self.get_experience_preferences_for_capability("GetExperiences", "", cancellation_token)
            .await
    }

    pub(crate) async fn native_get_experience_preferences(
        &self,
        cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
    ) -> Result<Option<ExperiencePreferencesMessage>, Error> {
        self.get_experience_preferences_for_capability(
            "ExperiencePreferences",
            "",
            cancellation_token,
        )
        .await
    }

    pub(crate) async fn native_set_experience_preferences_async(
        &self,
        preferences: ExperiencePreferencesMessage,
        cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
    ) -> Result<(), Error> {
        let payload = preferences.serialize()?;
        let _ = self
            .post_capability_map("ExperiencePreferences", payload, cancellation_token)
            .await?;
        Ok(())
    }

    pub(crate) async fn native_get_experience_permission(
        &self,
        experience_id: UUID,
        cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
    ) -> Result<Option<String>, Error> {
        let Some(map) = self
            .get_capability_map(
                "ExperiencePreferences",
                &format!("?{experience_id}"),
                cancellation_token,
            )
            .await?
        else {
            return Ok(None);
        };
        Self::extract_experience_permission(map, experience_id).map(Some)
    }

    pub(crate) async fn native_set_experience_permission(
        &self,
        experience_id: UUID,
        permission: String,
        cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
    ) -> Result<bool, Error> {
        let cancellation = cancellation_token.unwrap_or_else(|| self.client.cancellation_token());
        cancellation.throw_if_cancellation_requested()?;
        let Some(uri) = self.native_capability("ExperiencePreferences", &cancellation)? else {
            return Ok(false);
        };
        let payload = OSD::Map(HashMap::from([(
            experience_id.to_string(),
            OSD::Map(HashMap::from([(
                "permission".into(),
                OSD::String(permission),
            )])),
        )]));
        let response = self
            .client
            .http_caps_client()
            .put_with_uri_osd_format_osd_cancellation_token_i_progress(
                uri,
                libremetaverse_structured_data::OSDFormat::Xml,
                payload,
                cancellation,
                None,
            )
            .await;
        match response {
            Ok((response, _)) => Ok(response.is_success_status_code()),
            Err(Error::Cancelled) => Err(Error::Cancelled),
            Err(_) => Ok(false),
        }
    }

    pub(crate) async fn native_forget_experience_permission(
        &self,
        experience_id: UUID,
        cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
    ) -> Result<bool, Error> {
        let cancellation = cancellation_token.unwrap_or_else(|| self.client.cancellation_token());
        cancellation.throw_if_cancellation_requested()?;
        let Some(uri) = self.native_capability("ExperiencePreferences", &cancellation)? else {
            return Ok(false);
        };
        let response = self
            .client
            .http_caps_client()
            .delete_with_uri_string_bytes_cancellation_token_i_progress(
                Uri(format!("{}?{experience_id}", uri.0)),
                String::new(),
                Vec::new(),
                cancellation,
                None,
            )
            .await;
        match response {
            Ok((response, _)) => Ok(response.is_success_status_code()),
            Err(Error::Cancelled) => Err(Error::Cancelled),
            Err(_) => Ok(false),
        }
    }

    pub(crate) async fn native_get_experience_info(
        &self,
        experience_ids: Box<dyn Iterator<Item = UUID>>,
        cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
    ) -> Result<Option<ExperienceInfoMessage>, Error> {
        let ids: Vec<_> = experience_ids.collect();
        if ids.is_empty() {
            return Ok(Some(ExperienceInfoMessage::new()?));
        }
        let mut suffix = format!("/id/?page_size={}", ids.len());
        for id in ids {
            use std::fmt::Write as _;
            let _ = write!(suffix, "&public_id={id}");
        }
        self.native_get_experience_info_by_uri("GetExperienceInfo", &suffix, cancellation_token)
            .await
    }

    pub(crate) async fn native_find_experience_by_name(
        &self,
        query: String,
        page: Option<i32>,
        page_size: Option<i32>,
        cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
    ) -> Result<Option<ExperienceInfoMessage>, Error> {
        let suffix = format!(
            "?page={}&page_size={}&query={}",
            page.unwrap_or(0),
            page_size.unwrap_or(30),
            percent_encode(&query)
        );
        self.native_get_experience_info_by_uri("FindExperienceByName", &suffix, cancellation_token)
            .await
    }

    async fn native_get_experience_info_by_uri(
        &self,
        capability: &str,
        suffix: &str,
        cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
    ) -> Result<Option<ExperienceInfoMessage>, Error> {
        let Some(map) = self
            .get_capability_map(capability, suffix, cancellation_token)
            .await?
        else {
            return Ok(None);
        };
        let mut result = ExperienceInfoMessage::new()?;
        if result.deserialize(map).is_err() {
            return Ok(None);
        }
        self.inner
            .experience_info_received
            .emit(ExperienceInfoEventArgs::new(result.clone())?);
        Ok(Some(result))
    }

    pub(crate) async fn native_update_experience(
        &self,
        info: ExperienceInfo,
        cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
    ) -> Result<Option<ExperienceInfo>, Error> {
        let Some(map) = self
            .post_capability_map("UpdateExperience", info.serialize()?, cancellation_token)
            .await?
        else {
            return Ok(None);
        };
        let mut result = ExperienceInfoMessage::new()?;
        if result.deserialize(map).is_err() {
            return Ok(None);
        }
        Ok(result.experiences.into_iter().next())
    }

    pub(crate) async fn native_is_experience_role(
        &self,
        capability: &str,
        experience_id: UUID,
        cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
    ) -> Result<bool, Error> {
        let Some(map) = self
            .get_capability_map(
                capability,
                &format!("?experience_id={experience_id}"),
                cancellation_token,
            )
            .await?
        else {
            return Ok(false);
        };
        map.get("status")
            .map_or(Ok(false), |value| value.as_boolean())
            .or(Ok(false))
    }

    pub(crate) async fn native_get_region_experiences(
        &self,
        cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
    ) -> Result<Option<RegionExperiencesMessage>, Error> {
        let Some(map) = self
            .get_capability_map("RegionExperiences", "", cancellation_token)
            .await?
        else {
            return Ok(None);
        };
        let mut result = RegionExperiencesMessage::new()?;
        if result.deserialize(map).is_err() {
            return Ok(None);
        }
        self.native_set_last_region_experiences(Some(result.clone()));
        self.inner
            .region_experiences_updated
            .emit(RegionExperiencesEventArgs::new(result.clone())?);
        Ok(Some(result))
    }

    pub(crate) async fn native_chatter_box_accept_invite(
        &self,
        session_id: UUID,
        cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
    ) -> Result<(), Error> {
        let mut message = crate::messages::linden::ChatSessionAcceptInvitation::new()?;
        message.session_id = session_id;
        if self
            .post_capability_map(
                "ChatSessionRequest",
                message.serialize()?,
                cancellation_token,
            )
            .await?
            .is_none()
        {
            return Err(Error::InvalidOperation);
        }
        Ok(())
    }

    pub(crate) async fn native_start_im_conference(
        &self,
        participants: Vec<UUID>,
        session_id: UUID,
        cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
    ) -> Result<(), Error> {
        let mut message = crate::messages::linden::ChatSessionRequestStartConference::new()?;
        message.agents_block = participants;
        message.session_id = session_id;
        if self
            .post_capability_map(
                "ChatSessionRequest",
                message.serialize()?,
                cancellation_token,
            )
            .await?
            .is_none()
        {
            return Err(Error::InvalidOperation);
        }
        Ok(())
    }

    pub(crate) fn native_moderate_chat_sessions(
        &self,
        session_id: UUID,
        member_id: UUID,
        key: String,
        moderate: bool,
        cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
    ) -> Result<(), Error> {
        let cancellation = cancellation_token.unwrap_or_else(|| self.client.cancellation_token());
        cancellation.throw_if_cancellation_requested()?;
        let capability_available = self
            .client
            .network()
            .current_sim()
            .and_then(|simulator| simulator.native_caps())
            .and_then(|caps| {
                caps.capability_uri("ChatSessionRequest".into())
                    .ok()
                    .flatten()
            })
            .is_some();
        if !capability_available {
            return Err(Error::InvalidOperation);
        }
        let mut message = crate::messages::linden::ChatSessionRequestMuteUpdate::new()?;
        message.agent_id = member_id;
        message.request_key = key;
        message.request_value = moderate;
        message.session_id = session_id;
        let payload = message.serialize()?;
        let client = Arc::clone(&self.client);
        std::thread::Builder::new()
            .name("libremetaverse-chat-moderation".into())
            .spawn(move || {
                let Ok(runtime) = tokio::runtime::Builder::new_current_thread()
                    .enable_all()
                    .build()
                else {
                    return;
                };
                let Ok(manager) = AgentManager::native_new(Some(client)) else {
                    return;
                };
                let _ = runtime.block_on(manager.post_capability_map(
                    "ChatSessionRequest",
                    payload,
                    Some(cancellation),
                ));
            })
            .map(|_| ())
            .map_err(|_| Error::InvalidOperation)
    }

    pub(crate) async fn native_set_display_name(
        &self,
        old_name: String,
        new_name: String,
        cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
    ) -> Result<(), Error> {
        let mut message = crate::messages::linden::SetDisplayNameMessage::new()?;
        message.old_display_name = old_name;
        message.new_display_name = new_name;
        let _ = self
            .post_capability_map("SetDisplayName", message.serialize()?, cancellation_token)
            .await?;
        Ok(())
    }

    pub(crate) async fn native_update_agent_language(
        &self,
        language: String,
        is_public: bool,
        cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
    ) -> Result<(), Error> {
        let mut message = crate::messages::linden::UpdateAgentLanguageMessage::new()?;
        message.language = language;
        message.language_public = is_public;
        let _ = self
            .post_capability_map(
                "UpdateAgentLanguage",
                message.serialize()?,
                cancellation_token,
            )
            .await?;
        Ok(())
    }

    pub(crate) async fn native_set_agent_access_async(
        &self,
        access: String,
        cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
    ) -> Result<AgentAccessEventArgs, Error> {
        let payload = OSDMap::new_with_dictionary(HashMap::from([(
            "access_prefs".into(),
            OSD::Map(HashMap::from([("max".into(), OSD::String(access))])),
        )]))?;
        let Some(result) = self
            .post_capability_map("UpdateAgentInformation", payload, cancellation_token)
            .await?
        else {
            return AgentAccessEventArgs::new(false, self.native_agent_access());
        };
        let new_level = match result.get("access_prefs") {
            Some(OSD::Map(values)) => values
                .get("max")
                .map_or_else(|| Ok(self.native_agent_access()), OSD::as_string)?,
            _ => self.native_agent_access(),
        };
        self.native_set_agent_access(new_level.clone());
        AgentAccessEventArgs::new(true, new_level)
    }

    pub(crate) async fn native_set_hover_height_async(
        &self,
        hover_height: f64,
        cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
    ) -> Result<(), Error> {
        let payload = OSDMap::new_with_dictionary(HashMap::from([(
            "hover_height".into(),
            OSD::Real(hover_height),
        )]))?;
        if let Some(result) = self
            .post_capability_map("AgentPreferences", payload, cancellation_token)
            .await?
            && let Some(value) = result.get("hover_height")
            && let Ok(value) = value.as_real()
        {
            self.native_set_hover_height(value as f32);
        }
        Ok(())
    }

    pub(crate) async fn native_get_agent_preferences(
        &self,
        cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
    ) -> Result<Option<AgentPreferencesMessage>, Error> {
        let Some(map) = self
            .get_capability_map("AgentPreferences", "", cancellation_token)
            .await?
        else {
            return Ok(None);
        };
        let mut result = AgentPreferencesMessage::new()?;
        if result.deserialize(map).is_err() {
            return Ok(None);
        }
        self.native_set_hover_height(result.hover_height);
        *self
            .inner
            .agent_preferences
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(result.clone());
        self.inner
            .agent_preferences_updated
            .emit(AgentPreferencesEventArgs::new(result.clone())?);
        Ok(Some(result))
    }

    pub(crate) async fn native_get_avatar_render_info(
        &self,
        cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
    ) -> Result<Option<AvatarRenderInfoMessage>, Error> {
        let Some(map) = self
            .get_capability_map("AvatarRenderInfo", "", cancellation_token)
            .await?
        else {
            return Ok(None);
        };
        let mut result = AvatarRenderInfoMessage::new()?;
        if result.deserialize(map).is_err() {
            return Ok(None);
        }
        self.native_set_avatar_render_info(Some(result.clone()));
        self.inner
            .avatar_render_info_updated
            .emit(AvatarRenderInfoEventArgs::new(result.clone())?);
        Ok(Some(result))
    }

    pub(crate) async fn native_post_avatar_render_info(
        &self,
        agents: HashMap<UUID, (i32, bool)>,
        cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
    ) -> Result<(), Error> {
        if agents.is_empty() {
            return Ok(());
        }
        let message = AvatarRenderInfoMessage {
            agents: agents
                .into_iter()
                .map(|(id, (weight, too_complex))| {
                    (
                        id,
                        crate::messages::linden::AvatarRenderInfoMessageAvatarInfo {
                            too_complex,
                            weight,
                        },
                    )
                })
                .collect(),
            over_limit: 0,
            reporting_limit: 0,
        };
        let _ = self
            .post_capability_map("AvatarRenderInfo", message.serialize()?, cancellation_token)
            .await?;
        Ok(())
    }

    pub(crate) async fn native_post_local_avatar_render_info(
        &self,
        weight: i32,
        too_complex: bool,
        cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
    ) -> Result<(), Error> {
        self.native_post_avatar_render_info(
            HashMap::from([(self.native_agent_id(), (weight, too_complex))]),
            cancellation_token,
        )
        .await
    }

    pub(crate) async fn native_get_viewer_benefits(
        &self,
        cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
    ) -> Result<Option<ViewerBenefitsMessage>, Error> {
        let Some(map) = self
            .get_capability_map("ViewerBenefits", "", cancellation_token)
            .await?
        else {
            return Ok(None);
        };
        let account_level_benefits = map.get("account_level_benefits").and_then(|value| {
            let OSD::Map(values) = value else {
                return None;
            };
            crate::AccountLevelBenefits::new_with_osd_map(
                OSDMap::new_with_dictionary(values.clone()).ok()?,
            )
            .ok()
        });
        let mut result = ViewerBenefitsMessage::new()?;
        if result.deserialize(map).is_err() {
            return Ok(None);
        }
        if let Some(benefits) = account_level_benefits {
            self.native_set_benefits(benefits);
        }
        self.native_set_viewer_benefits(Some(result.clone()));
        self.inner
            .viewer_benefits_updated
            .emit(ViewerBenefitsEventArgs::new(result.clone())?);
        Ok(Some(result))
    }

    pub(crate) async fn native_send_viewer_stats(
        &self,
        stats: ViewerStatsMessage,
        cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
    ) -> Result<(), Error> {
        let _ = self
            .post_capability_map("ViewerStats", stats.serialize()?, cancellation_token)
            .await?;
        Ok(())
    }

    pub(crate) async fn native_request_nav_mesh_generation_status(
        &self,
        cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
    ) -> Result<Option<crate::messages::linden::NavMeshStatusUpdateMessage>, Error> {
        let Some(map) = self
            .get_capability_map("NavMeshGenerationStatus", "", cancellation_token)
            .await?
        else {
            return Ok(None);
        };
        let mut result = crate::messages::linden::NavMeshStatusUpdateMessage::new()?;
        if result.deserialize(map).is_err() {
            return Ok(None);
        }
        self.native_set_last_nav_mesh_status(Some(result.clone()));
        Ok(Some(result))
    }

    pub(crate) async fn native_request_nav_mesh_rebake(
        &self,
        cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
    ) -> Result<(), Error> {
        let empty = OSDMap::new_with_dictionary(HashMap::new())?;
        let _ = self
            .post_capability_map("NavMeshGenerationStatus", empty, cancellation_token)
            .await?;
        Ok(())
    }

    pub(crate) async fn native_get_product_info(
        &self,
        cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
    ) -> Result<Option<ProductInfoRequestMessage>, Error> {
        let cancellation = cancellation_token.unwrap_or_else(|| self.client.cancellation_token());
        let Some(uri) = self.native_capability("ProductInfoRequest", &cancellation)? else {
            return Ok(None);
        };
        let response = self
            .client
            .http_caps_client()
            .get(uri, cancellation, None)
            .await;
        let (response, data) = match response {
            Ok(value) => value,
            Err(Error::Cancelled) => return Err(Error::Cancelled),
            Err(_) => return Ok(None),
        };
        if !response.is_success_status_code() {
            return Ok(None);
        }
        let map = match OSDParser::deserialize_with_bytes(data) {
            Ok(OSD::Map(map)) => map,
            Ok(OSD::Array(products)) => HashMap::from([("products".into(), OSD::Array(products))]),
            _ => return Ok(None),
        };
        let mut result = ProductInfoRequestMessage::new()?;
        if result
            .deserialize(OSDMap::new_with_dictionary(map)?)
            .is_err()
        {
            return Ok(None);
        }
        self.native_set_product_info(Some(result.clone()));
        self.inner
            .product_info_updated
            .emit(ProductInfoEventArgs::new(result.clone())?);
        Ok(Some(result))
    }

    pub(crate) async fn native_get_attachment_resources(
        &self,
        cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
    ) -> Result<
        (
            bool,
            Option<crate::messages::linden::AttachmentResourcesMessage>,
        ),
        Error,
    > {
        let cancellation = cancellation_token.unwrap_or_else(|| self.client.cancellation_token());
        cancellation.throw_if_cancellation_requested()?;
        let Some(capability) = self.native_capability("AttachmentResources", &cancellation)? else {
            return Ok((false, None));
        };
        let (response, data) = self
            .client
            .http_caps_client()
            .get(capability, cancellation, None)
            .await?;
        if !response.is_success_status_code() {
            return Ok((false, None));
        }
        let Ok(parsed) = OSDParser::deserialize_with_bytes(data) else {
            return Ok((false, None));
        };
        match crate::messages::linden::AttachmentResourcesMessage::from_osd(parsed) {
            Ok(info) => Ok((true, Some(info))),
            Err(_) => Ok((false, None)),
        }
    }

    pub(crate) async fn native_retrieve_instant_messages(
        &self,
        cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
    ) -> Result<(), Error> {
        let cancellation = cancellation_token.unwrap_or_else(|| self.client.cancellation_token());
        let Some(offline) = self.native_capability("ReadOfflineMsgs", &cancellation)? else {
            return self.native_retrieve_instant_messages_legacy();
        };
        if self
            .native_capability("AcceptFriendship", &cancellation)?
            .is_none()
            || self
                .native_capability("AcceptGroupInvite", &cancellation)?
                .is_none()
        {
            return self.native_retrieve_instant_messages_legacy();
        }
        let (_, data) = self
            .client
            .http_caps_client()
            .get(offline, cancellation, None)
            .await?;
        self.deliver_offline_messages(&data)
    }

    fn native_capability(
        &self,
        name: &str,
        cancellation: &libremetaverse_types::compat::CancellationToken,
    ) -> Result<Option<Uri>, Error> {
        let Some(caps) = self
            .client
            .network()
            .current_sim()
            .and_then(|simulator| simulator.native_caps())
        else {
            return Ok(None);
        };
        let deadline = Instant::now() + Duration::from_secs(2);
        loop {
            if let Some(uri) = caps.capability_uri(name.to_owned())? {
                return Ok(Some(uri));
            }
            if caps.seed_request_finished() || Instant::now() >= deadline {
                return Ok(None);
            }
            cancellation.throw_if_cancellation_requested()?;
            std::thread::sleep(Duration::from_millis(1));
        }
    }

    pub(crate) async fn native_query_experiences_on_parcel(
        &self,
        parcel_local_id: i32,
        experience_ids: Box<dyn Iterator<Item = UUID>>,
        cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
    ) -> Result<Option<HashMap<UUID, bool>>, Error> {
        let cancellation = cancellation_token.unwrap_or_else(|| self.client.cancellation_token());
        cancellation.throw_if_cancellation_requested()?;
        let Some(capability) = self.native_capability("ExperienceQuery", &cancellation)? else {
            return Ok(None);
        };
        let ids: Vec<_> = experience_ids.collect();
        let mut uri = format!("{}?parcelid={parcel_local_id}", capability.0);
        if !ids.is_empty() {
            uri.push_str("&experiences=");
            uri.push_str(
                &ids.iter()
                    .map(ToString::to_string)
                    .collect::<Vec<_>>()
                    .join(","),
            );
        }
        let response = self
            .client
            .http_caps_client()
            .get(Uri(uri), cancellation.clone(), None)
            .await;
        let (response, data) = match response {
            Ok(result) => result,
            Err(Error::Cancelled) => return Err(Error::Cancelled),
            Err(_) => return Ok(None),
        };
        if !response.is_success_status_code() {
            return Ok(None);
        }
        let OSD::Map(mut map) = OSDParser::deserialize_with_bytes(data)? else {
            return Ok(None);
        };
        let Some(OSD::Map(experiences)) = map.remove("experiences") else {
            return Ok(None);
        };
        let mut result = HashMap::with_capacity(experiences.len());
        for (id, allowed) in experiences {
            let id = UUID::new_with_string(id)?;
            result.insert(id, allowed.as_boolean()?);
        }
        Ok(Some(result))
    }

    #[allow(clippy::too_many_arguments)]
    pub(crate) async fn native_send_postcard(
        &self,
        jpeg_image_data: Vec<u8>,
        to_email: String,
        from_name: String,
        subject: String,
        message: String,
        global_position: Vector3,
        cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
    ) -> Result<bool, Error> {
        let cancellation = cancellation_token.unwrap_or_else(|| self.client.cancellation_token());
        cancellation.throw_if_cancellation_requested()?;
        let Some(capability) = self.native_capability("SendPostcard", &cancellation)? else {
            return Ok(false);
        };
        let mut postcard = crate::messages::linden::SendPostcardMessage::new()?;
        postcard.from_name = from_name;
        postcard.to_email = to_email;
        postcard.subject = subject;
        postcard.message = message;
        postcard.global_position = global_position;
        let payload = OSD::Map(postcard.serialize()?.snapshot());
        let first = self
            .client
            .http_caps_client()
            .post_with_uri_osd_format_osd_cancellation_token_i_progress(
                capability,
                libremetaverse_structured_data::OSDFormat::Xml,
                payload,
                cancellation.clone(),
                None,
            )
            .await;
        let (response, data) = match first {
            Ok(value) => value,
            Err(Error::Cancelled) => return Err(Error::Cancelled),
            Err(_) => return Ok(false),
        };
        if !response.is_success_status_code() {
            return Ok(false);
        }
        let Ok(OSD::Map(mut response)) = OSDParser::deserialize_with_bytes(data) else {
            return Ok(false);
        };
        let Some(uploader) = response.remove("uploader") else {
            return Ok(false);
        };
        let uploader = uploader.as_string()?;
        if uploader.is_empty() {
            return Ok(false);
        }
        let upload = self
            .client
            .http_caps_client()
            .post_with_uri_string_bytes_cancellation_token_i_progress(
                Uri(uploader),
                "application/octet-stream".into(),
                jpeg_image_data,
                cancellation,
                None,
            )
            .await;
        let (response, data) = match upload {
            Ok(value) => value,
            Err(Error::Cancelled) => return Err(Error::Cancelled),
            Err(_) => return Ok(false),
        };
        if !response.is_success_status_code() {
            return Ok(false);
        }
        let Ok(OSD::Map(mut response)) = OSDParser::deserialize_with_bytes(data) else {
            return Ok(false);
        };
        Ok(response
            .remove("state")
            .is_some_and(|state| state.as_string().as_deref() == Ok("complete")))
    }

    fn native_retrieve_instant_messages_legacy(&self) -> Result<(), Error> {
        let mut packet = crate::packets::RetrieveInstantMessagesPacket::new_with_constructor()?;
        packet.agent_data.agent_id = self.native_agent_id();
        packet.agent_data.session_id = self.native_session_id();
        let data = packet.to_bytes_with_method()?;
        let length = i32::try_from(data.len()).map_err(|_| Error::Argument)?;
        let simulator = self
            .client
            .network()
            .current_sim()
            .ok_or(Error::InvalidOperation)?;
        simulator.native_send_packet_data(
            data,
            length,
            crate::packets::PacketType::RetrieveInstantMessages,
            false,
        )
    }

    fn deliver_offline_messages(&self, data: &[u8]) -> Result<(), Error> {
        let root = OSDParser::deserialize_with_bytes(data.to_vec())?;
        let messages = match root {
            OSD::Array(values) => values.into_iter().next().and_then(|value| match value {
                OSD::Array(messages) => Some(messages),
                _ => None,
            }),
            OSD::Map(mut map) => map.remove("messages").and_then(|value| match value {
                OSD::Array(messages) => Some(messages),
                _ => None,
            }),
            _ => None,
        }
        .ok_or(Error::Argument)?;

        for value in messages {
            let OSD::Map(map) = value else {
                return Err(Error::Argument);
            };
            let message = offline_message(&map)?;
            if message.group_im {
                self.group_chat_sessions
                    .write()
                    .unwrap_or_else(std::sync::PoisonError::into_inner)
                    .entry(message.im_session_id)
                    .or_default();
            }
            self.inner.instant_message.emit(InstantMessageEventArgs {
                im: message,
                simulator: None,
            });
        }
        Ok(())
    }

    pub(crate) fn extract_experience_permission(
        map: OSDMap,
        experience_id: UUID,
    ) -> Result<String, Error> {
        if osd_uuid_array_contains(&map, "experiences", experience_id)? {
            return Ok("Allow".into());
        }
        if osd_uuid_array_contains(&map, "blocked", experience_id)? {
            return Ok("Block".into());
        }
        Ok("Forget".into())
    }
}

struct UdpAssetAbortGuard {
    simulator: Simulator,
    transfer_id: UUID,
    completed: bool,
}

impl UdpAssetAbortGuard {
    fn new(simulator: Simulator, transfer_id: UUID) -> Self {
        Self {
            simulator,
            transfer_id,
            completed: false,
        }
    }

    fn complete(&mut self) {
        self.completed = true;
    }
}

impl Drop for UdpAssetAbortGuard {
    fn drop(&mut self) {
        if self.completed {
            return;
        }
        let Ok(mut packet) = crate::packets::TransferAbortPacket::new_with_constructor() else {
            return;
        };
        packet.transfer_info.channel_type = crate::ChannelType::Asset as i32;
        packet.transfer_info.transfer_id = self.transfer_id;
        let Ok(data) = packet.to_bytes_with_method() else {
            return;
        };
        let Ok(length) = i32::try_from(data.len()) else {
            return;
        };
        let do_zerocode = data
            .first()
            .is_some_and(|flags| flags & crate::Helpers::MSG_ZEROCODED != 0);
        let _ = self.simulator.native_send_packet_data(
            data,
            length,
            crate::packets::PacketType::TransferAbort,
            do_zerocode,
        );
    }
}

fn required<'a>(map: &'a HashMap<String, OSD>, key: &str) -> Result<&'a OSD, Error> {
    map.get(key).ok_or(Error::Argument)
}

fn gesture_chat_channel(text: &str) -> (i32, String) {
    let Some(rest) = text.strip_prefix('/') else {
        return (0, text.to_owned());
    };
    let bytes = rest.as_bytes();
    let mut end = usize::from(bytes.first() == Some(&b'-'));
    let digit_start = end;
    while bytes.get(end).is_some_and(u8::is_ascii_digit) {
        end += 1;
    }
    if end == digit_start {
        return (0, text.to_owned());
    }
    let Ok(channel) = rest[..end].parse::<i32>() else {
        return (0, text.to_owned());
    };
    let message = rest[end..]
        .trim_start_matches(char::is_whitespace)
        .to_owned();
    (channel, message)
}

fn decode_raw_packet<T: crate::packet_catalog::GeneratedPacket>(data: &[u8]) -> Result<T, Error> {
    let mut packet = T::new_generated();
    let mut position = 0_i32;
    let mut packet_end = i32::try_from(data.len()).map_err(|_| Error::Argument)? - 1;
    let mut zero_buffer =
        vec![0_u8; crate::udp_transport::UdpTransportConfig::default().max_decoded_packet_size];
    packet.decode_from_bytes(
        data,
        &mut position,
        &mut packet_end,
        Some(zero_buffer.as_mut_slice()),
    )?;
    Ok(packet)
}

fn wire_string(mut bytes: Vec<u8>) -> String {
    while bytes.last() == Some(&0) {
        bytes.pop();
    }
    String::from_utf8_lossy(&bytes).into_owned()
}

fn chat_audible_level(value: u8) -> Result<crate::ChatAudibleLevel, Error> {
    match value {
        0 => Ok(crate::ChatAudibleLevel::Barely),
        1 => Ok(crate::ChatAudibleLevel::Fully),
        u8::MAX => Ok(crate::ChatAudibleLevel::Not),
        _ => Err(Error::Argument),
    }
}

fn chat_source_type(value: u8) -> Result<crate::ChatSourceType, Error> {
    match value {
        0 => Ok(crate::ChatSourceType::System),
        1 => Ok(crate::ChatSourceType::Agent),
        2 => Ok(crate::ChatSourceType::Object),
        _ => Err(Error::Argument),
    }
}

fn chat_type(value: u8) -> Result<crate::ChatType, Error> {
    match value {
        0 => Ok(crate::ChatType::Whisper),
        1 => Ok(crate::ChatType::Normal),
        2 => Ok(crate::ChatType::Shout),
        4 => Ok(crate::ChatType::StartTyping),
        5 => Ok(crate::ChatType::StopTyping),
        6 => Ok(crate::ChatType::Debug),
        8 => Ok(crate::ChatType::OwnerSay),
        9 => Ok(crate::ChatType::RegionSayTo),
        u8::MAX => Ok(crate::ChatType::RegionSay),
        _ => Err(Error::Argument),
    }
}

fn instant_message_online(value: u8) -> Result<InstantMessageOnline, Error> {
    match value {
        0 => Ok(InstantMessageOnline::Online),
        1 => Ok(InstantMessageOnline::Offline),
        _ => Err(Error::Argument),
    }
}

fn instant_message_dialog(value: u8) -> Result<InstantMessageDialog, Error> {
    use InstantMessageDialog as Dialog;
    match value {
        0 => Ok(Dialog::MessageFromAgent),
        1 => Ok(Dialog::MessageBox),
        3 => Ok(Dialog::GroupInvitation),
        4 => Ok(Dialog::InventoryOffered),
        5 => Ok(Dialog::InventoryAccepted),
        6 => Ok(Dialog::InventoryDeclined),
        7 => Ok(Dialog::GroupVote),
        9 => Ok(Dialog::TaskInventoryOffered),
        10 => Ok(Dialog::TaskInventoryAccepted),
        11 => Ok(Dialog::TaskInventoryDeclined),
        12 => Ok(Dialog::NewUserDefault),
        13 => Ok(Dialog::SessionAdd),
        14 => Ok(Dialog::SessionOfflineAdd),
        15 => Ok(Dialog::SessionGroupStart),
        16 => Ok(Dialog::SessionCardlessStart),
        17 => Ok(Dialog::SessionSend),
        18 => Ok(Dialog::SessionDrop),
        19 => Ok(Dialog::MessageFromObject),
        20 => Ok(Dialog::BusyAutoResponse),
        21 => Ok(Dialog::ConsoleAndChatHistory),
        22 => Ok(Dialog::RequestTeleport),
        23 => Ok(Dialog::AcceptTeleport),
        24 => Ok(Dialog::DenyTeleport),
        25 => Ok(Dialog::GodLikeRequestTeleport),
        26 => Ok(Dialog::RequestLure),
        28 => Ok(Dialog::GotoUrl),
        29 => Ok(Dialog::Session911Start),
        30 => Ok(Dialog::Lure911),
        31 => Ok(Dialog::FromTaskAsAlert),
        32 => Ok(Dialog::GroupNotice),
        33 => Ok(Dialog::GroupNoticeInventoryAccepted),
        34 => Ok(Dialog::GroupNoticeInventoryDeclined),
        35 => Ok(Dialog::GroupInvitationAccept),
        36 => Ok(Dialog::GroupInvitationDecline),
        37 => Ok(Dialog::GroupNoticeRequested),
        38 => Ok(Dialog::FriendshipOffered),
        39 => Ok(Dialog::FriendshipAccepted),
        40 => Ok(Dialog::FriendshipDeclined),
        41 => Ok(Dialog::StartTyping),
        42 => Ok(Dialog::StopTyping),
        _ => Err(Error::Argument),
    }
}

fn split_utf8(message: &str, max_bytes: usize, max_parts: usize) -> Vec<String> {
    assert!(max_bytes > 0, "split size must be at least one byte");
    if message.is_empty() {
        return vec![String::new()];
    }
    let mut parts = Vec::new();
    let mut start = 0;
    while start < message.len() && parts.len() < max_parts {
        let proposed = start.saturating_add(max_bytes).min(message.len());
        let mut end = proposed;
        while end > start && !message.is_char_boundary(end) {
            end -= 1;
        }
        let end = if end == start {
            message[start..]
                .char_indices()
                .nth(1)
                .map_or(message.len(), |(offset, _)| start + offset)
        } else {
            end
        };
        parts.push(message[start..end].to_owned());
        start = end;
    }
    parts
}

fn percent_encode(value: &str) -> String {
    let mut encoded = String::with_capacity(value.len());
    for byte in value.bytes() {
        if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~') {
            encoded.push(char::from(byte));
        } else {
            use std::fmt::Write as _;
            let _ = write!(encoded, "%{byte:02X}");
        }
    }
    encoded
}

fn optional_uuid(map: &HashMap<String, OSD>, key: &str) -> Result<UUID, Error> {
    map.get(key).map_or(Ok(UUID::zero()), OSD::as_uuid)
}

fn optional_i32(map: &HashMap<String, OSD>, key: &str, fallback: i32) -> Result<i32, Error> {
    map.get(key).map_or(Ok(fallback), OSD::as_integer)
}

fn optional_u32(map: &HashMap<String, OSD>, key: &str, fallback: u32) -> Result<u32, Error> {
    map.get(key).map_or(Ok(fallback), OSD::as_u_integer)
}

fn offline_message(map: &HashMap<String, OSD>) -> Result<InstantMessage, Error> {
    let dialog = match required(map, "dialog")?.as_integer()? {
        0 => InstantMessageDialog::MessageFromAgent,
        17 => InstantMessageDialog::SessionSend,
        _ => return Err(Error::Argument),
    };
    let offline = match optional_i32(map, "offline", 1)? {
        0 => InstantMessageOnline::Online,
        1 => InstantMessageOnline::Offline,
        _ => return Err(Error::Argument),
    };
    let position = if let Some(value) = map.get("position") {
        value.as_vector3()?
    } else {
        Vector3 {
            #[allow(clippy::cast_possible_truncation)]
            x: required(map, "local_x")?.as_real()? as f32,
            #[allow(clippy::cast_possible_truncation)]
            y: required(map, "local_y")?.as_real()? as f32,
            #[allow(clippy::cast_possible_truncation)]
            z: required(map, "local_z")?.as_real()? as f32,
        }
    };
    let ticks = optional_i32(map, "timestamp", 0)?.max(0);
    Ok(InstantMessage {
        binary_bucket: map
            .get("binary_bucket")
            .map_or_else(|| Ok(vec![0]), OSD::as_binary)?,
        dialog,
        from_agent_id: required(map, "from_agent_id")?.as_uuid()?,
        from_agent_name: required(map, "from_agent_name")?.as_string()?,
        group_im: map
            .get("from_group")
            .is_some_and(|value| value.as_boolean() == Ok(true)),
        im_session_id: required(map, "transaction-id")?.as_uuid()?,
        message: required(map, "message")?.as_string()?,
        offline,
        parent_estate_id: optional_u32(map, "parent_estate_id", 1)?,
        position,
        region_id: optional_uuid(map, "region_id")?,
        timestamp: UNIX_EPOCH + Duration::from_secs(u64::try_from(ticks).unwrap_or_default()),
        to_agent_id: required(map, "to_agent_id")?.as_uuid()?,
    })
}

fn osd_uuid_array_contains(map: &OSDMap, key: &str, id: UUID) -> Result<bool, Error> {
    let Some(OSD::Array(values)) = map.get(key) else {
        return Ok(false);
    };
    for value in values {
        if value.as_uuid()? == id {
            return Ok(true);
        }
    }
    Ok(false)
}

#[cfg(test)]
#[allow(clippy::too_many_lines)] // Protocol scenarios remain readable as contiguous exchanges.
mod tests {
    use super::*;
    use std::net::{IpAddr, Ipv4Addr, SocketAddr};

    fn loopback(port: u16) -> SocketAddr {
        SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port)
    }

    fn simulator(client: &GridClient, endpoint: SocketAddr) -> Simulator {
        Simulator::native_new(client.clone(), endpoint, 0x1000, Some(256), Some(256))
            .expect("simulator")
    }

    fn transport_wire(data: Vec<u8>) -> Vec<u8> {
        if data
            .first()
            .is_none_or(|flags| flags & crate::Helpers::MSG_ZEROCODED == 0)
        {
            return data;
        }
        let mut encoded = vec![0_u8; data.len().saturating_mul(2).saturating_add(2)];
        let length = crate::packet_wire::zero_encode(
            Some(&data),
            i32::try_from(data.len()).unwrap(),
            Some(&mut encoded),
        )
        .unwrap();
        encoded.truncate(usize::try_from(length).unwrap());
        encoded
    }

    #[test]
    fn chat_uses_utf8_safe_protocol_chunks_and_exact_identity() {
        let client = Arc::new(GridClient::new().expect("client"));
        let manager = AgentManager::native_new(Some(client)).unwrap();
        manager.native_set_agent_id(
            UUID::new_with_string("11111111-1111-1111-1111-111111111111".into()).unwrap(),
        );
        manager.native_set_session_id(
            UUID::new_with_string("22222222-2222-2222-2222-222222222222".into()).unwrap(),
        );
        let message = "é".repeat(600);
        let packets = manager
            .native_chat_payloads(message.clone(), 7, ChatType::Normal, Some(true))
            .unwrap();

        let mut reconstructed = String::new();
        assert_eq!(packets.len(), 2);
        for (_, data) in packets {
            let data = transport_wire(data);
            let packet = decode_raw_packet::<crate::packets::ChatFromViewerPacket>(&data).unwrap();
            assert_eq!(packet.agent_data.agent_id, manager.native_agent_id());
            assert_eq!(packet.agent_data.session_id, manager.native_session_id());
            assert_eq!(packet.chat_data.channel, 7);
            assert_eq!(packet.chat_data.type_, ChatType::Normal as u8);
            assert!(packet.chat_data.message.len() <= MAX_CHAT_BYTES);
            reconstructed.push_str(&wire_string(packet.chat_data.message));
        }
        assert_eq!(reconstructed, message);
    }

    #[test]
    fn gesture_chat_channel_matches_reference_regex_behavior() {
        assert_eq!(gesture_chat_channel("hello"), (0, "hello".into()));
        assert_eq!(gesture_chat_channel("/5   hello"), (5, "hello".into()));
        assert_eq!(gesture_chat_channel("/-42answer"), (-42, "answer".into()));
        assert_eq!(gesture_chat_channel("/x nope"), (0, "/x nope".into()));
        assert_eq!(
            gesture_chat_channel("/999999999999999999 text"),
            (0, "/999999999999999999 text".into())
        );
    }

    #[test]
    fn instant_message_debug_redacts_message_and_binary_payload() {
        let message = InstantMessage {
            binary_bucket: b"private-bucket".to_vec(),
            dialog: InstantMessageDialog::MessageFromAgent,
            from_agent_id: UUID::random().unwrap(),
            from_agent_name: "Sender Resident".into(),
            group_im: false,
            im_session_id: UUID::random().unwrap(),
            message: "private-message".into(),
            offline: InstantMessageOnline::Online,
            parent_estate_id: 1,
            position: Vector3::zero(),
            region_id: UUID::random().unwrap(),
            timestamp: UNIX_EPOCH,
            to_agent_id: UUID::random().unwrap(),
        };
        let diagnostic = format!("{message:?}");
        assert!(!diagnostic.contains("private-message"));
        assert!(!diagnostic.contains("private-bucket"));
        assert!(!diagnostic.contains("Sender Resident"));
        assert!(diagnostic.contains("message_len"));
    }

    #[test]
    fn raw_agent_packets_update_state_and_raise_callbacks_outside_locks() {
        let client = GridClient::new().unwrap();
        let sim = simulator(&client, loopback(13001));
        let inner = AgentManagerInner::default();

        let chats = Arc::new(Mutex::new(Vec::new()));
        let chat_values = Arc::clone(&chats);
        let _chat_subscription = inner.chat_from_simulator.subscribe(Arc::new(move |event| {
            chat_values.lock().unwrap().push(event.message());
        }));
        let mut chat = crate::packets::ChatFromSimulatorPacket::new_with_constructor().unwrap();
        chat.chat_data.message = b"hello\0".to_vec();
        chat.chat_data.from_name = b"Grid Resident\0".to_vec();
        chat.chat_data.audible = 1;
        chat.chat_data.chat_type = ChatType::Normal as u8;
        chat.chat_data.source_type = crate::ChatSourceType::Agent as u8;
        inner.handle_raw_packet(crate::network_manager::RawPacketReceivedEventArgs {
            packet_type: crate::packets::PacketType::ChatFromSimulator,
            data: transport_wire(chat.to_bytes_with_method().unwrap()),
            simulator: sim.clone(),
        });
        assert_eq!(*chats.lock().unwrap(), ["hello"]);

        let ims = Arc::new(Mutex::new(Vec::new()));
        let im_values = Arc::clone(&ims);
        let _im_subscription = inner.instant_message.subscribe(Arc::new(move |event| {
            im_values.lock().unwrap().push(event.im().message);
        }));
        let mut im = crate::packets::ImprovedInstantMessagePacket::new_with_constructor().unwrap();
        im.message_block.dialog = InstantMessageDialog::MessageFromAgent as u8;
        im.message_block.offline = InstantMessageOnline::Online as u8;
        im.message_block.message = b"private\0".to_vec();
        inner
            .handle_instant_message_packet(&transport_wire(im.to_bytes_with_method().unwrap()), sim)
            .unwrap();
        assert_eq!(*ims.lock().unwrap(), ["private"]);

        let balances = Arc::new(Mutex::new(Vec::new()));
        let balance_values = Arc::clone(&balances);
        let _balance_subscription = inner.money_balance.subscribe(Arc::new(move |event| {
            balance_values.lock().unwrap().push(event.balance());
        }));
        let mut money = crate::packets::MoneyBalanceReplyPacket::new_with_constructor().unwrap();
        money.money_data.money_balance = 1234;
        money.money_data.description = b"payment\0".to_vec();
        money.transaction_info.item_description = b"item\0".to_vec();
        inner.handle_raw_packet(crate::network_manager::RawPacketReceivedEventArgs {
            packet_type: crate::packets::PacketType::MoneyBalanceReply,
            data: transport_wire(money.to_bytes_with_method().unwrap()),
            simulator: simulator(&client, loopback(13002)),
        });
        assert_eq!(*balances.lock().unwrap(), [1234]);
        assert_eq!(*inner.balance.read().unwrap(), 1234);

        let identity = Arc::new(Mutex::new(Vec::new()));
        let identity_values = Arc::clone(&identity);
        let _identity_subscription = inner.agent_data_reply.subscribe(Arc::new(move |event| {
            identity_values.lock().unwrap().push((
                event.first_name(),
                event.last_name(),
                event.active_group_id(),
                event.group_title(),
                event.group_powers(),
                event.group_name(),
            ));
        }));
        let mut identity_packet =
            crate::packets::AgentDataUpdatePacket::new_with_constructor().unwrap();
        identity_packet.agent_data.agent_id = UUID::random().unwrap();
        identity_packet.agent_data.first_name = b"Test\0".to_vec();
        identity_packet.agent_data.last_name = b"Resident\0".to_vec();
        identity_packet.agent_data.active_group_id = UUID::random().unwrap();
        identity_packet.agent_data.group_title = b"Builder\0".to_vec();
        identity_packet.agent_data.group_name = b"Builders\0".to_vec();
        identity_packet.agent_data.group_powers = 0x1234;
        inner.handle_raw_packet(crate::network_manager::RawPacketReceivedEventArgs {
            packet_type: crate::packets::PacketType::AgentDataUpdate,
            data: transport_wire(identity_packet.to_bytes_with_method().unwrap()),
            simulator: simulator(&client, loopback(13005)),
        });
        assert_eq!(
            *identity.lock().unwrap(),
            [(
                "Test".into(),
                "Resident".into(),
                identity_packet.agent_data.active_group_id,
                "Builder".into(),
                crate::GroupPowers(0x1234),
                "Builders".into(),
            )]
        );
        assert_eq!(*inner.first_name.read().unwrap(), "Test");
        assert_eq!(*inner.last_name.read().unwrap(), "Resident");

        let animation_id = UUID::random().unwrap();
        let changed = Arc::new(Mutex::new(Vec::new()));
        let changed_values = Arc::clone(&changed);
        let _animation_subscription = inner.animations_changed.subscribe(Arc::new(move |event| {
            changed_values.lock().unwrap().push(event.animations());
        }));
        let mut animations = crate::packets::AvatarAnimationPacket::new_with_constructor().unwrap();
        animations.sender.id = UUID::random().unwrap();
        let mut animation =
            crate::packets::AvatarAnimationPacketAnimationListBlock::new_with_constructor()
                .unwrap();
        animation.anim_id = animation_id;
        animation.anim_sequence_id = 17;
        animations.animation_list.push(animation);
        inner.handle_raw_packet(crate::network_manager::RawPacketReceivedEventArgs {
            packet_type: crate::packets::PacketType::AvatarAnimation,
            data: transport_wire(animations.to_bytes_with_method().unwrap()),
            simulator: simulator(&client, loopback(13006)),
        });
        assert_eq!(
            *changed.lock().unwrap(),
            [HashMap::from([(animation_id, 17)])]
        );
        assert_eq!(
            *inner.signaled_animations.read().unwrap(),
            HashMap::from([(animation_id, 17)])
        );
    }

    #[test]
    fn group_chat_caps_events_track_members_and_raise_compatible_events() {
        let client = Arc::new(GridClient::new().unwrap());
        let manager = Arc::new(AgentManager::native_new(Some(Arc::clone(&client))).unwrap());
        let sim = simulator(&client, loopback(13003));
        let session_id =
            UUID::new_with_string("33333333-3333-3333-3333-333333333333".into()).unwrap();
        let agent_id =
            UUID::new_with_string("44444444-4444-4444-4444-444444444444".into()).unwrap();

        let joined = Arc::new(Mutex::new(Vec::new()));
        let joined_values = Arc::clone(&joined);
        let _joined = manager.native_subscribe_group_chat_joined(Arc::new(move |event| {
            joined_values.lock().unwrap().push((
                event.session_id(),
                event.session_name(),
                event.tmp_session_id(),
                event.success(),
            ));
        }));
        let manager_for_added = Arc::clone(&manager);
        let added = Arc::new(Mutex::new(Vec::new()));
        let added_values = Arc::clone(&added);
        let _added = manager.native_subscribe_chat_session_member_added(Arc::new(move |event| {
            // Taking the collection read lock proves callbacks are not invoked
            // while the membership write lock is held.
            assert!(
                manager_for_added
                    .group_chat_sessions
                    .read()
                    .unwrap()
                    .contains_key(&event.session_id())
            );
            added_values.lock().unwrap().push(event.agent_id());
        }));
        let left = Arc::new(Mutex::new(Vec::new()));
        let left_values = Arc::clone(&left);
        let _left = manager.native_subscribe_chat_session_member_left(Arc::new(move |event| {
            left_values.lock().unwrap().push(event.agent_id());
        }));

        let mut started =
            crate::messages::linden::ChatterBoxSessionStartReplyMessage::new().unwrap();
        started.session_id = session_id;
        started.session_name = "Group Chat".into();
        started.temp_session_id = UUID::random().unwrap();
        started.success = true;
        let mut started_roundtrip =
            crate::messages::linden::ChatterBoxSessionStartReplyMessage::new().unwrap();
        started_roundtrip
            .deserialize(started.serialize().unwrap())
            .unwrap();
        assert_eq!(started_roundtrip.session_id, session_id);
        client
            .network()
            .dispatch_caps_event("ChatterBoxSessionStartReply", &started, sim.clone());
        assert_eq!(
            *joined.lock().unwrap(),
            [(
                session_id,
                "Group Chat".into(),
                started.temp_session_id,
                true
            )]
        );
        assert!(
            manager
                .group_chat_sessions
                .read()
                .unwrap()
                .contains_key(&session_id)
        );

        let mut entered =
            crate::messages::linden::ChatterBoxSessionAgentListUpdatesMessage::new().unwrap();
        entered.session_id = session_id;
        let mut member = crate::messages::linden::
            ChatterBoxSessionAgentListUpdatesMessageAgentUpdatesBlock::new()
            .unwrap();
        member.agent_id = agent_id;
        member.transition = "ENTER".into();
        member.can_voice_chat = true;
        member.is_moderator = true;
        member.mute_text = true;
        entered.updates.push(member);
        client.network().dispatch_caps_event(
            "ChatterBoxSessionAgentListUpdates",
            &entered,
            sim.clone(),
        );
        assert_eq!(*added.lock().unwrap(), [agent_id]);
        assert_eq!(
            manager.group_chat_sessions.read().unwrap()[&session_id],
            [ChatSessionMember {
                avatar_key: agent_id,
                can_voice_chat: true,
                is_moderator: true,
                mute_text: true,
                mute_voice: false,
            }]
        );

        let mut departed =
            crate::messages::linden::ChatterBoxSessionAgentListUpdatesMessage::new().unwrap();
        departed.session_id = session_id;
        let mut member = crate::messages::linden::
            ChatterBoxSessionAgentListUpdatesMessageAgentUpdatesBlock::new()
            .unwrap();
        member.agent_id = agent_id;
        member.transition = "LEAVE".into();
        departed.updates.push(member);
        client
            .network()
            .dispatch_caps_event("ChatterBoxSessionAgentListUpdates", &departed, sim);
        assert_eq!(*left.lock().unwrap(), [agent_id]);
        assert!(manager.group_chat_sessions.read().unwrap()[&session_id].is_empty());
    }

    #[test]
    fn caps_invitation_and_display_name_reply_preserve_event_payloads() {
        let client = Arc::new(GridClient::new().unwrap());
        let manager = AgentManager::native_new(Some(Arc::clone(&client))).unwrap();
        let sim = simulator(&client, loopback(13004));

        let (sender, receiver) = std::sync::mpsc::channel();
        let _im = manager.native_subscribe_im(Arc::new(move |event| {
            let _ = sender.send(event.im());
        }));
        let mut invitation = crate::messages::linden::ChatterBoxInvitationMessage::new().unwrap();
        invitation.from_agent_id = UUID::random().unwrap();
        invitation.from_agent_name = "Inviter Resident".into();
        invitation.to_agent_id = UUID::random().unwrap();
        invitation.im_session_id = UUID::random().unwrap();
        invitation.message = "conference invitation".into();
        invitation.dialog = InstantMessageDialog::SessionSend;
        invitation.offline = InstantMessageOnline::Online;
        invitation.timestamp = UNIX_EPOCH + Duration::from_secs(42);
        let mut invitation_roundtrip =
            crate::messages::linden::ChatterBoxInvitationMessage::new().unwrap();
        invitation_roundtrip
            .deserialize(invitation.serialize().unwrap())
            .unwrap();
        assert_eq!(invitation_roundtrip.im_session_id, invitation.im_session_id);
        client
            .network()
            .dispatch_caps_event("ChatterBoxInvitation", &invitation, sim.clone());
        let delivered = receiver.recv_timeout(Duration::from_secs(2)).unwrap();
        assert_eq!(delivered.from_agent_id, invitation.from_agent_id);
        assert_eq!(delivered.im_session_id, invitation.im_session_id);
        assert_eq!(delivered.message, invitation.message);
        assert!(
            manager
                .group_chat_sessions
                .read()
                .unwrap()
                .contains_key(&invitation.im_session_id)
        );

        let display_events = Arc::new(Mutex::new(Vec::new()));
        let display_values = Arc::clone(&display_events);
        let _display = manager.native_subscribe_set_display_name_reply(Arc::new(move |event| {
            display_values.lock().unwrap().push((
                event.status(),
                event.reason(),
                event.display_name().display_name(),
            ));
        }));
        let mut reply = crate::messages::linden::SetDisplayNameReplyMessage::new().unwrap();
        reply.status = 200;
        reply.reason = "ok".into();
        reply.display_name.set_id(invitation.to_agent_id);
        reply
            .display_name
            .set_display_name(Some("New Display Name".into()));
        client
            .network()
            .dispatch_caps_event("SetDisplayNameReply", &reply, sim);
        assert_eq!(
            *display_events.lock().unwrap(),
            [(200, "ok".into(), Some("New Display Name".into()))]
        );
    }

    #[test]
    fn profile_capability_payload_matches_reference_keys() {
        let profile = crate::AvatarAvatarProperties {
            about_text: "about".into(),
            born_on: String::new(),
            charter_member: String::new(),
            first_life_image: UUID::new_with_string("11111111-1111-1111-1111-111111111111".into())
                .unwrap(),
            first_life_text: "first".into(),
            flags: crate::ProfileFlags(
                crate::ProfileFlags::ALLOW_PUBLISH.0 | crate::ProfileFlags::MATURE_PUBLISH.0,
            ),
            partner: UUID::zero(),
            profile_image: UUID::new_with_string("22222222-2222-2222-2222-222222222222".into())
                .unwrap(),
            profile_url: "https://example.invalid/profile".into(),
        };
        let payload = AgentManager::profile_payload(&profile).unwrap();
        assert_eq!(
            payload.get("sl_about_text").unwrap().as_string().unwrap(),
            "about"
        );
        assert_eq!(
            payload.get("fl_about_text").unwrap().as_string().unwrap(),
            "first"
        );
        assert!(payload.get("allow_publish").unwrap().as_boolean().unwrap());
        assert_eq!(
            payload.get("sl_image_id").unwrap().as_uuid().unwrap(),
            profile.profile_image
        );
        assert_eq!(
            payload.get("fl_image_id").unwrap().as_uuid().unwrap(),
            profile.first_life_image
        );
    }

    #[test]
    fn mute_list_parser_preserves_types_names_and_flags() {
        let resident =
            UUID::new_with_string("11111111-1111-1111-1111-111111111111".into()).unwrap();
        let group = UUID::new_with_string("22222222-2222-2222-2222-222222222222".into()).unwrap();
        let parsed = parse_mute_list(&format!(
            "1 {resident} Example Resident|3\n3 {group} Example Group|8\ninvalid line\n"
        ));
        assert_eq!(parsed.len(), 2);
        assert_eq!(
            parsed[&format!("{resident}|Example Resident")].type_,
            crate::MuteType::Resident
        );
        assert_eq!(
            parsed[&format!("{resident}|Example Resident")].flags,
            crate::MuteFlags(3)
        );
        assert_eq!(
            parsed[&format!("{group}|Example Group")].type_,
            crate::MuteType::Group
        );
        assert_eq!(
            parsed[&format!("{group}|Example Group")].flags,
            crate::MuteFlags(8)
        );
    }

    #[test]
    fn nav_mesh_caps_event_updates_last_status_before_callback() {
        let client = Arc::new(GridClient::new().unwrap());
        let manager = Arc::new(AgentManager::native_new(Some(Arc::clone(&client))).unwrap());
        let sim = simulator(&client, loopback(13007));
        let region_id = UUID::random().unwrap();
        let observed = Arc::new(Mutex::new(Vec::new()));
        let observed_values = Arc::clone(&observed);
        let manager_for_callback = Arc::clone(&manager);
        let _subscription =
            manager.native_subscribe_nav_mesh_status_update(Arc::new(move |event| {
                let stored = manager_for_callback
                    .native_last_nav_mesh_status()
                    .expect("status is stored before the event");
                observed_values.lock().unwrap().push((
                    event.message().status,
                    event.simulator().ip_end_point(),
                    stored.version,
                ));
            }));
        let mut status = crate::messages::linden::NavMeshStatusUpdateMessage::new().unwrap();
        status.region_id = region_id;
        status.status = "building".into();
        status.version = 9;
        client
            .network()
            .dispatch_caps_event("NavMeshStatusUpdate", &status, sim.clone());

        let stored = manager.native_last_nav_mesh_status().unwrap();
        assert_eq!(stored.region_id, region_id);
        assert_eq!(stored.status, "building");
        assert_eq!(stored.version, 9);
        assert_eq!(
            *observed.lock().unwrap(),
            [("building".into(), sim.ip_end_point(), 9)]
        );
    }

    #[tokio::test(flavor = "current_thread")]
    async fn fake_grid_serves_gesture_capability_and_observes_udp_actions() {
        use crate::packets::{
            Packet, PacketAckPacket, PacketAckPacketPacketsBlock, PacketType, RegionHandshakePacket,
        };
        use libremetaverse_types::compat::{HttpMessageHandler, HttpResponse, Uri};
        use std::collections::BTreeMap;
        use std::net::UdpSocket;
        use std::sync::mpsc;

        fn packet_type(bytes: &[u8]) -> Option<PacketType> {
            let mut packet_end = i32::try_from(bytes.len()).ok()?.checked_sub(1)?;
            Packet::build_packet_with_bytes_int32_bytes(
                bytes.to_vec(),
                &mut packet_end,
                vec![0; 8192],
            )
            .ok()
            .map(|packet| packet.type_)
        }

        fn ack(sequence: u32) -> Vec<u8> {
            let mut packet = PacketAckPacket::new_with_constructor().unwrap();
            packet.packets = vec![PacketAckPacketPacketsBlock { id: sequence }];
            let mut bytes = packet.to_bytes_with_method().unwrap();
            bytes[0] &= !(crate::Helpers::MSG_RELIABLE | crate::Helpers::MSG_ZEROCODED);
            bytes
        }

        let animation_id = UUID::random().unwrap();
        let sound_id = UUID::random().unwrap();
        let gesture_id = UUID::random().unwrap();
        let mut gesture = crate::assets::AssetGesture::new_with_constructor().unwrap();
        gesture.sequence = vec![
            crate::assets::GestureStep::Chat(crate::assets::GestureStepChat {
                text: "/7 hello grid".into(),
            }),
            crate::assets::GestureStep::Animation(crate::assets::GestureStepAnimation {
                animation_start: true,
                id: animation_id,
                name: "Wave".into(),
            }),
            crate::assets::GestureStep::Sound(crate::assets::GestureStepSound {
                id: sound_id,
                name: "Chime".into(),
            }),
            crate::assets::GestureStep::EOF(crate::assets::GestureStepEOF),
        ];
        gesture.encode().unwrap();
        let gesture_bytes = gesture.asset_data().to_vec();

        let socket = UdpSocket::bind(loopback(0)).expect("fake grid UDP socket");
        socket
            .set_read_timeout(Some(Duration::from_secs(10)))
            .unwrap();
        let endpoint = socket.local_addr().unwrap();
        let (udp_sender, udp_receiver) = mpsc::channel();
        let server = std::thread::spawn(move || {
            let mut buffer = [0_u8; 8192];
            let (length, client_endpoint) = socket.recv_from(&mut buffer).expect("UseCircuitCode");
            assert_eq!(
                packet_type(&buffer[..length]),
                Some(PacketType::UseCircuitCode)
            );
            let sequence = u32::from_be_bytes(buffer[1..5].try_into().unwrap());
            socket.send_to(&ack(sequence), client_endpoint).unwrap();

            let mut handshake = RegionHandshakePacket::new_with_constructor().unwrap();
            handshake.region_info.sim_name = b"Agent Test Region\0".to_vec();
            let mut handshake_bytes = handshake.to_bytes_with_method().unwrap();
            handshake_bytes[0] &= !(crate::Helpers::MSG_RELIABLE | crate::Helpers::MSG_ZEROCODED);
            socket.send_to(&handshake_bytes, client_endpoint).unwrap();

            let mut observed = Vec::new();
            while observed.len() < 3 {
                let (length, _) = socket.recv_from(&mut buffer).expect("agent UDP packet");
                if buffer[0] & crate::Helpers::MSG_RELIABLE != 0 {
                    let sequence = u32::from_be_bytes(buffer[1..5].try_into().unwrap());
                    socket.send_to(&ack(sequence), client_endpoint).unwrap();
                }
                let Some(kind) = packet_type(&buffer[..length]) else {
                    continue;
                };
                if matches!(
                    kind,
                    PacketType::ChatFromViewer
                        | PacketType::AgentAnimation
                        | PacketType::SoundTrigger
                ) && !observed.contains(&kind)
                {
                    observed.push(kind);
                }
            }
            udp_sender.send(observed).unwrap();
        });

        let requests = Arc::new(Mutex::new(Vec::new()));
        let recorded = Arc::clone(&requests);
        let handler = HttpMessageHandler::new(move |request, _| {
            let recorded = Arc::clone(&recorded);
            let gesture_bytes = gesture_bytes.clone();
            async move {
                let is_seed = request.uri.0.contains("/seed");
                recorded.lock().unwrap().push(request);
                let body = if is_seed {
                    OSDParser::serialize_llsd_xml_bytes(OSD::Map(HashMap::from([(
                        "ViewerAsset".into(),
                        OSD::Uri(Uri("https://caps.test/viewer-asset".into())),
                    )])))
                    .unwrap()
                } else {
                    gesture_bytes
                };
                HttpResponse {
                    status_code: 200,
                    headers: BTreeMap::new(),
                    content_type: None,
                    body,
                }
            }
        });
        let mut client = GridClient::new().unwrap();
        client.native_set_http_caps_client(crate::HttpCapsClient::new(handler).unwrap());
        client.settings().timing().login_timeout = 2_000;
        let client = Arc::new(client);
        let mut network = client.network();
        network.set_circuit_code(0x1234_5678);
        let simulator = network
            .native_connect(
                endpoint,
                0x1000,
                true,
                Some(Uri("https://caps.test/seed".into())),
                256,
                256,
            )
            .unwrap()
            .unwrap();
        let caps = simulator.clone().caps.expect("seed capabilities");
        let deadline = Instant::now() + Duration::from_secs(2);
        while caps.capability_uri("ViewerAsset".into()).unwrap().is_none()
            && Instant::now() < deadline
        {
            std::thread::sleep(Duration::from_millis(2));
        }
        assert_eq!(
            caps.capability_uri("ViewerAsset".into()).unwrap(),
            Some(Uri("https://caps.test/viewer-asset".into())),
            "seed requests: {:?}",
            requests.lock().unwrap()
        );
        let manager = AgentManager::native_new(Some(Arc::clone(&client))).unwrap();
        manager.native_play_gesture(gesture_id, None).await.unwrap();

        let observed = udp_receiver.recv_timeout(Duration::from_secs(3)).unwrap();
        assert_eq!(
            observed,
            [
                PacketType::ChatFromViewer,
                PacketType::AgentAnimation,
                PacketType::SoundTrigger,
            ]
        );
        let requests = requests.lock().unwrap();
        assert!(
            requests
                .iter()
                .any(|request| request.uri.0 == "https://caps.test/seed")
        );
        assert!(requests.iter().any(|request| {
            request.uri.0 == format!("https://caps.test/viewer-asset?gesture_id={gesture_id}")
        }));
        drop(requests);
        simulator.native_disconnect(false).unwrap();
        server.join().unwrap();
    }
}
