//! Native inventory protocol manager.
//!
//! This module owns request correlation and wire behavior.  The hierarchy in
//! [`crate::inventory`] remains a transport-free store, which lets packet and
//! capability replies be validated before they become observable local state.
//! Store snapshots and subscriber lists are cloned before asynchronous work or
//! event delivery, so callbacks are not invoked while an inventory lock is held.

#![allow(clippy::missing_errors_doc)] // Public signatures mirror the C# API.
#![allow(clippy::must_use_candidate)]
#![allow(clippy::needless_pass_by_value)]
#![allow(clippy::too_many_arguments)] // Capability calls preserve mapped request fields.
#![allow(clippy::too_many_lines)] // Protocol parsers intentionally keep each wire state machine together.
#![allow(clippy::unnecessary_wraps)] // Mapped C# methods require Result-shaped Rust APIs.

use crate::agent_manager::{EventRegistry, InstantMessage, InstantMessageEventArgs};
use crate::client_core::ClientWeakHandle;
use crate::inventory::{
    Inventory, InventoryAnimation, InventoryAttachment, InventoryBase, InventoryCallingCard,
    InventoryCategory, InventoryFolder, InventoryGesture, InventoryItem, InventoryLSL,
    InventoryLandmark, InventoryMaterial, InventoryNotecard, InventoryObject, InventorySettings,
    InventorySnapshot, InventorySound, InventoryTexture, InventoryWearable, Permissions,
};
use crate::messages::linden::{CopyInventoryFromNotecardMessage, ScriptRunningReplyMessage};
use crate::network_manager::{CapsEventQueueCallback, RawPacketReceivedEventArgs, Simulator};
use crate::packet_catalog::GeneratedPacket;
use crate::packets::{
    AbortXferPacket, BulkUpdateInventoryPacket, ConfirmXferPacketPacket,
    CopyInventoryFromNotecardPacket, CopyInventoryFromNotecardPacketInventoryDataBlock,
    CopyInventoryItemPacket, CopyInventoryItemPacketInventoryDataBlock,
    CreateInventoryFolderPacket, CreateInventoryItemPacket, DeRezObjectPacket,
    DeRezObjectPacketObjectDataBlock, FetchInventoryPacket, FetchInventoryPacketInventoryDataBlock,
    FetchInventoryReplyPacket, GetScriptRunningPacket, InventoryDescendentsPacket,
    LinkInventoryItemPacket, MoveInventoryFolderPacket,
    MoveInventoryFolderPacketInventoryDataBlock, MoveInventoryItemPacket,
    MoveInventoryItemPacketInventoryDataBlock, MoveTaskInventoryPacket, PacketType,
    PurgeInventoryDescendentsPacket, RemoveInventoryObjectsPacket,
    RemoveInventoryObjectsPacketFolderDataBlock, RemoveInventoryObjectsPacketItemDataBlock,
    RemoveTaskInventoryPacket, ReplyTaskInventoryPacket, RequestTaskInventoryPacket,
    RequestXferPacket, RezObjectPacket, RezRestoreToWorldPacket, RezScriptPacket,
    SaveAssetIntoInventoryPacket, SendXferPacketPacket, SetScriptRunningPacket,
    UpdateCreateInventoryItemPacket, UpdateInventoryFolderPacket,
    UpdateInventoryFolderPacketFolderDataBlock, UpdateInventoryItemPacket,
    UpdateInventoryItemPacketInventoryDataBlock, UpdateTaskInventoryPacket,
};
use crate::{
    DeRezDestination, GridClient, InstantMessageDialog, InstantMessageOnline, InventoryItemClass,
    InventoryObjectClass, InventorySortOrder, PermissionMask,
};
use crate::{LoginResponseData, NetworkManagerLoginResponseCallback};
use futures_channel::oneshot;
use futures_util::future::{Either, select};
use libremetaverse_structured_data::{OSD, OSDParser};
use libremetaverse_types::compat::{
    CancellationToken, EventHandler, ExternalError, Subscription, Task, Uri,
};
use libremetaverse_types::{
    AssetType, EnumValueMetadata, Error, FolderType, InventoryType, Quaternion, SaleType, UUID,
    Utils, Vector3, Vector3d, WearableType,
};
use std::collections::{HashMap, HashSet};
use std::fmt;
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
use std::sync::{Arc, Condvar, Mutex, RwLock};
use std::thread::JoinHandle;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};

const MAGIC_ID_TEXT: &str = "3c115e51-04f4-523c-9fa6-98aff1034730";
const MAX_TASK_INVENTORY_BYTES: usize = 16 * 1024 * 1024;
const MAX_TASK_INVENTORY_ENTRIES: usize = 100_000;
const MAX_PENDING_REQUESTS: usize = 4_096;
const CALLBACK_TIMEOUT: Duration = Duration::from_mins(1);
const CALLBACK_SWEEP_INTERVAL: Duration = Duration::from_millis(100);
const MAX_INVENTORY_RECURSION_DEPTH: usize = 512;
const MAX_INVENTORY_TRAVERSAL: usize = 100_000;

macro_rules! fill_inventory_wire_block {
    ($block:expr, $item:expr, $transaction_id:expr, $type_value:expr) => {{
        let item = $item;
        let permissions = item.permissions();
        $block.item_id = item.base.uuid();
        $block.folder_id = item.base.parent_uuid();
        $block.creator_id = item.creator_id();
        $block.owner_id = item.base.owner_id();
        $block.group_id = item.group_id();
        $block.base_mask = permissions.base_mask.0;
        $block.owner_mask = permissions.owner_mask.0;
        $block.group_mask = permissions.group_mask.0;
        $block.everyone_mask = permissions.everyone_mask.0;
        $block.next_owner_mask = permissions.next_owner_mask.0;
        $block.group_owned = item.group_owned();
        $block.transaction_id = $transaction_id;
        $block.type_ = $type_value;
        $block.inv_type = item.inventory_type().0;
        $block.flags = item.flags();
        $block.sale_type = item.sale_type() as u8;
        $block.sale_price = item.sale_price();
        $block.name = Utils::string_to_bytes(item.base.name())?;
        $block.description = Utils::string_to_bytes(item.description())?;
        $block.creation_date = unix_seconds_i32(item.creation_date());
    }};
}

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

fn read<T>(value: &RwLock<T>) -> std::sync::RwLockReadGuard<'_, T> {
    value
        .read()
        .unwrap_or_else(std::sync::PoisonError::into_inner)
}

fn write<T>(value: &RwLock<T>) -> std::sync::RwLockWriteGuard<'_, T> {
    value
        .write()
        .unwrap_or_else(std::sync::PoisonError::into_inner)
}

fn expire_waiters<T>(
    requests: &Mutex<HashMap<UUID, Vec<T>>>,
    now: Instant,
    deadline: impl Fn(&T) -> Instant,
) -> Vec<T> {
    let mut requests = mutex(requests);
    let keys: Vec<_> = requests.keys().copied().collect();
    let mut expired = Vec::new();
    for key in keys {
        if let Some(waiters) = requests.get_mut(&key) {
            let mut index = 0;
            while index < waiters.len() {
                if deadline(&waiters[index]) <= now {
                    expired.push(waiters.swap_remove(index));
                } else {
                    index += 1;
                }
            }
            if waiters.is_empty() {
                requests.remove(&key);
            }
        }
    }
    expired
}

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

fn decode_packet<T: GeneratedPacket>(bytes: &[u8]) -> Result<T, Error> {
    let mut packet = T::new_generated();
    let mut position = 0_i32;
    let mut packet_end = i32::try_from(bytes.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(
        bytes,
        &mut position,
        &mut packet_end,
        Some(zero_buffer.as_mut_slice()),
    )?;
    Ok(packet)
}

fn unix_seconds_i64(value: SystemTime) -> i64 {
    match value.duration_since(UNIX_EPOCH) {
        Ok(duration) => i64::try_from(duration.as_secs()).unwrap_or(i64::MAX),
        Err(error) => -i64::try_from(error.duration().as_secs()).unwrap_or(i64::MAX),
    }
}

fn unix_seconds_u32_wrapping(value: SystemTime) -> u32 {
    let bytes = unix_seconds_i64(value).to_le_bytes();
    u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
}

fn uuid_crc(value: UUID) -> u32 {
    let mut value = value;
    value.crc().unwrap_or_default()
}

fn system_time_from_unix_i32(seconds: i32) -> SystemTime {
    if seconds >= 0 {
        UNIX_EPOCH + Duration::from_secs(seconds.unsigned_abs().into())
    } else {
        UNIX_EPOCH - Duration::from_secs(seconds.unsigned_abs().into())
    }
}

fn unix_seconds_i32(value: SystemTime) -> i32 {
    i32::try_from(unix_seconds_i64(value)).unwrap_or_else(|_| {
        if value >= UNIX_EPOCH {
            i32::MAX
        } else {
            i32::MIN
        }
    })
}

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

fn folder_type_from_i8(value: i8) -> FolderType {
    folder_type_from_i8_checked(value).unwrap_or(FolderType::None)
}

fn folder_type_from_i8_checked(value: i8) -> Option<FolderType> {
    match value {
        -1 => Some(FolderType::None),
        0 => Some(FolderType::Texture),
        1 => Some(FolderType::Sound),
        2 => Some(FolderType::CallingCard),
        3 => Some(FolderType::Landmark),
        5 => Some(FolderType::Clothing),
        6 => Some(FolderType::Object),
        7 => Some(FolderType::Notecard),
        8 => Some(FolderType::Root),
        9 => Some(FolderType::OldRoot),
        10 => Some(FolderType::LSLText),
        13 => Some(FolderType::BodyPart),
        14 => Some(FolderType::Trash),
        15 => Some(FolderType::Snapshot),
        16 => Some(FolderType::LostAndFound),
        20 => Some(FolderType::Animation),
        21 => Some(FolderType::Gesture),
        23 => Some(FolderType::Favorites),
        26 => Some(FolderType::EnsembleStart),
        45 => Some(FolderType::EnsembleEnd),
        46 => Some(FolderType::CurrentOutfit),
        47 => Some(FolderType::Outfit),
        48 => Some(FolderType::MyOutfits),
        49 => Some(FolderType::Mesh),
        50 => Some(FolderType::Inbox),
        51 => Some(FolderType::Outbox),
        52 => Some(FolderType::BasicRoot),
        53 => Some(FolderType::MarketplaceListings),
        54 => Some(FolderType::MarketplaceStock),
        55 => Some(FolderType::MarketplaceVersion),
        56 => Some(FolderType::Settings),
        57 => Some(FolderType::Material),
        100 => Some(FolderType::Suitcase),
        _ => None,
    }
}

fn sale_type_from_u8(value: u8) -> SaleType {
    match value {
        1 => SaleType::Original,
        2 => SaleType::Copy,
        3 => SaleType::Contents,
        _ => SaleType::Not,
    }
}

#[derive(Clone)]
struct InventoryWireData {
    asset_id: UUID,
    base_mask: u32,
    creation_date: i32,
    creator_id: UUID,
    description: Vec<u8>,
    everyone_mask: u32,
    flags: u32,
    folder_id: UUID,
    group_id: UUID,
    group_mask: u32,
    group_owned: bool,
    inv_type: i8,
    item_id: UUID,
    name: Vec<u8>,
    next_owner_mask: u32,
    owner_id: UUID,
    owner_mask: u32,
    sale_price: i32,
    sale_type: u8,
    type_: i8,
}

fn inventory_item_from_wire(data: InventoryWireData) -> Result<InventoryItem, Error> {
    let mut inventory_type = InventoryType(data.inv_type);
    let asset_type = asset_type_from_i8(data.type_);
    if inventory_type == InventoryType::TEXTURE
        && matches!(asset_type, AssetType::Object | AssetType::Mesh)
    {
        inventory_type = InventoryType::ATTACHMENT;
    }
    let mut item = InventoryItem::new_with_inventory_type_uuid(inventory_type, data.item_id)?;
    item.set_asset_uuid(data.asset_id);
    item.set_asset_type(asset_type);
    item.set_creation_date(system_time_from_unix_i32(data.creation_date));
    item.set_creator_id(data.creator_id);
    item.set_description(wire_string(data.description));
    item.set_flags(data.flags);
    item.set_group_id(data.group_id);
    item.set_group_owned(data.group_owned);
    item.set_inventory_type(inventory_type);
    item.base.set_name(wire_string(data.name));
    item.base.set_owner_id(data.owner_id);
    item.base.set_parent_uuid(data.folder_id);
    item.set_permissions(Permissions::new(
        data.base_mask,
        data.everyone_mask,
        data.group_mask,
        data.next_owner_mask,
        data.owner_mask,
    )?);
    item.set_sale_price(data.sale_price);
    item.set_sale_type(sale_type_from_u8(data.sale_type));
    Ok(item)
}

fn typed_inventory_item(item: InventoryItem) -> Box<dyn InventoryObjectClass> {
    macro_rules! boxed {
        ($kind:ty) => {{
            let mut value = <$kind>::new(item.base.uuid()).expect("known inventory constructor");
            value.base = item;
            Box::new(value) as Box<dyn InventoryObjectClass>
        }};
    }
    match item.inventory_type() {
        InventoryType::TEXTURE => boxed!(InventoryTexture),
        InventoryType::SOUND => boxed!(InventorySound),
        InventoryType::CALLING_CARD => boxed!(InventoryCallingCard),
        InventoryType::LANDMARK => boxed!(InventoryLandmark),
        InventoryType::OBJECT => boxed!(InventoryObject),
        InventoryType::NOTECARD => boxed!(InventoryNotecard),
        InventoryType::CATEGORY => boxed!(InventoryCategory),
        InventoryType::LSL => boxed!(InventoryLSL),
        InventoryType::SNAPSHOT => boxed!(InventorySnapshot),
        InventoryType::ATTACHMENT => boxed!(InventoryAttachment),
        InventoryType::WEARABLE => boxed!(InventoryWearable),
        InventoryType::ANIMATION => boxed!(InventoryAnimation),
        InventoryType::GESTURE => boxed!(InventoryGesture),
        InventoryType::SETTINGS => boxed!(InventorySettings),
        InventoryType::MATERIAL => boxed!(InventoryMaterial),
        _ => Box::new(item),
    }
}

/// C# `FindObjectByPathReplyEventArgs`.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FindObjectByPathReplyEventArgs {
    path: String,
    inventory_object_id: UUID,
}

impl FindObjectByPathReplyEventArgs {
    pub fn new(path: String, inventory_object_id: UUID) -> Result<Self, Error> {
        Ok(Self {
            path,
            inventory_object_id,
        })
    }
    pub fn inventory_object_id(&self) -> UUID {
        self.inventory_object_id
    }
    pub fn path(&self) -> String {
        self.path.clone()
    }
}

/// C# `FolderUpdatedEventArgs`.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct FolderUpdatedEventArgs {
    folder_id: UUID,
    success: bool,
}

impl FolderUpdatedEventArgs {
    pub fn new(folder_id: UUID, success: bool) -> Result<Self, Error> {
        Ok(Self { folder_id, success })
    }
    pub fn folder_id(&self) -> UUID {
        self.folder_id
    }
    pub fn success(&self) -> bool {
        self.success
    }
}

/// C# `ItemReceivedEventArgs`.
#[derive(Clone, Debug)]
pub struct ItemReceivedEventArgs {
    item: InventoryItem,
}

impl ItemReceivedEventArgs {
    pub fn new(item: InventoryItem) -> Result<Self, Error> {
        Ok(Self { item })
    }
    pub fn item(&self) -> InventoryItem {
        self.item.clone()
    }
}

/// C# `SaveAssetToInventoryEventArgs`.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct SaveAssetToInventoryEventArgs {
    item_id: UUID,
    new_asset_id: UUID,
}

impl SaveAssetToInventoryEventArgs {
    pub fn new(item_id: UUID, new_asset_id: UUID) -> Result<Self, Error> {
        Ok(Self {
            item_id,
            new_asset_id,
        })
    }
    pub fn item_id(&self) -> UUID {
        self.item_id
    }
    pub fn new_asset_id(&self) -> UUID {
        self.new_asset_id
    }
}

/// C# `ScriptRunningReplyEventArgs`.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ScriptRunningReplyEventArgs {
    object_id: UUID,
    script_id: UUID,
    is_mono: bool,
    is_running: bool,
}

impl ScriptRunningReplyEventArgs {
    pub fn new(
        object_id: UUID,
        sctript_id: UUID,
        is_mono: bool,
        is_running: bool,
    ) -> Result<Self, Error> {
        Ok(Self {
            object_id,
            script_id: sctript_id,
            is_mono,
            is_running,
        })
    }
    pub fn is_mono(&self) -> bool {
        self.is_mono
    }
    pub fn is_running(&self) -> bool {
        self.is_running
    }
    pub fn object_id(&self) -> UUID {
        self.object_id
    }
    pub fn script_id(&self) -> UUID {
        self.script_id
    }
}

/// C# `TaskInventoryReplyEventArgs`.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TaskInventoryReplyEventArgs {
    item_id: UUID,
    serial: i16,
    asset_filename: String,
}

impl TaskInventoryReplyEventArgs {
    pub fn new(item_id: UUID, serial: i16, asset_filename: String) -> Result<Self, Error> {
        Ok(Self {
            item_id,
            serial,
            asset_filename,
        })
    }
    pub fn asset_filename(&self) -> String {
        self.asset_filename.clone()
    }
    pub fn item_id(&self) -> UUID {
        self.item_id
    }
    pub fn serial(&self) -> i16 {
        self.serial
    }
}

/// C# `TaskItemReceivedEventArgs`.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TaskItemReceivedEventArgs {
    item_id: UUID,
    folder_id: UUID,
    creator_id: UUID,
    asset_id: UUID,
    type_: InventoryType,
}

impl TaskItemReceivedEventArgs {
    pub fn new(
        item_id: UUID,
        folder_id: UUID,
        creator_id: UUID,
        asset_id: UUID,
        type_: InventoryType,
    ) -> Result<Self, Error> {
        Ok(Self {
            item_id,
            folder_id,
            creator_id,
            asset_id,
            type_,
        })
    }
    pub fn asset_id(&self) -> UUID {
        self.asset_id
    }
    pub fn creator_id(&self) -> UUID {
        self.creator_id
    }
    pub fn folder_id(&self) -> UUID {
        self.folder_id
    }
    pub fn item_id(&self) -> UUID {
        self.item_id
    }
    pub fn type_(&self) -> InventoryType {
        self.type_
    }
}

/// C# `InventoryObjectOfferedEventArgs`.
#[derive(Clone, Debug)]
pub struct InventoryObjectOfferedEventArgs {
    offer: InstantMessage,
    asset_type: AssetType,
    object_id: UUID,
    from_task: bool,
    decision: Arc<RwLock<InventoryOfferDecision>>,
}

#[derive(Clone, Copy, Debug)]
struct InventoryOfferDecision {
    folder_id: UUID,
    accept: bool,
}

impl InventoryObjectOfferedEventArgs {
    pub fn new(
        offer_details: InstantMessage,
        type_: AssetType,
        object_id: UUID,
        from_task: bool,
        folder_id: UUID,
    ) -> Result<Self, Error> {
        Ok(Self {
            offer: offer_details,
            asset_type: type_,
            object_id,
            from_task,
            decision: Arc::new(RwLock::new(InventoryOfferDecision {
                folder_id,
                accept: false,
            })),
        })
    }
    pub fn accept(&self) -> bool {
        read(&self.decision).accept
    }
    pub fn set_accept(&mut self, value: bool) {
        write(&self.decision).accept = value;
    }
    pub fn asset_type(&self) -> AssetType {
        self.asset_type
    }
    pub fn folder_id(&self) -> UUID {
        read(&self.decision).folder_id
    }
    pub fn set_folder_id(&mut self, value: UUID) {
        write(&self.decision).folder_id = value;
    }
    pub fn from_task(&self) -> bool {
        self.from_task
    }
    pub fn object_id(&self) -> UUID {
        self.object_id
    }
    pub fn offer(&self) -> InstantMessage {
        self.offer.clone()
    }
}

/// Result returned by the C#-compatible multi-item copy APIs.
#[derive(Clone, Debug, Default)]
pub struct InventoryManagerCopyItemsResult {
    copied_items: Option<Vec<InventoryBase>>,
    error: Option<ExternalError>,
    success: bool,
}

impl InventoryManagerCopyItemsResult {
    pub fn new() -> Result<Self, Error> {
        Ok(Self::default())
    }
    pub fn copied_items(&self) -> Option<Vec<InventoryBase>> {
        self.copied_items.clone()
    }
    pub fn set_copied_items(&mut self, value: Option<Vec<InventoryBase>>) {
        self.copied_items = value;
    }
    pub fn error(&self) -> Option<ExternalError> {
        self.error.clone()
    }
    pub fn set_error(&mut self, value: Option<ExternalError>) {
        self.error = value;
    }
    pub fn success(&self) -> bool {
        self.success
    }
    pub fn set_success(&mut self, value: bool) {
        self.success = value;
    }
}

/// Result returned by the C#-compatible folder refresh API.
#[derive(Clone, Debug, Default)]
pub struct InventoryManagerFolderUpdateResult {
    contents: Option<Vec<InventoryBase>>,
    folder_id: UUID,
    success: bool,
}

impl InventoryManagerFolderUpdateResult {
    pub fn new() -> Result<Self, Error> {
        Ok(Self::default())
    }
    pub fn contents(&self) -> Option<Vec<InventoryBase>> {
        self.contents.clone()
    }
    pub fn set_contents(&mut self, value: Option<Vec<InventoryBase>>) {
        self.contents = value;
    }
    pub fn folder_id(&self) -> UUID {
        self.folder_id
    }
    pub fn set_folder_id(&mut self, value: UUID) {
        self.folder_id = value;
    }
    pub fn success(&self) -> bool {
        self.success
    }
    pub fn set_success(&mut self, value: bool) {
        self.success = value;
    }
}

struct PendingCreated {
    deadline: Instant,
    sender: oneshot::Sender<Option<InventoryItem>>,
}

struct PendingCopy {
    deadline: Instant,
    expected: usize,
    copied: Vec<InventoryBase>,
    sender: oneshot::Sender<InventoryManagerCopyItemsResult>,
}

struct PendingFetch {
    request_id: u64,
    deadline: Instant,
    sender: oneshot::Sender<Option<InventoryItem>>,
}

struct PendingTaskReply {
    request_id: u64,
    deadline: Instant,
    sender: oneshot::Sender<String>,
}

struct PendingOffer {
    request_id: u64,
    deadline: Instant,
    sender: oneshot::Sender<InventoryObjectOfferedEventArgs>,
}

struct TaskXfer {
    deadline: Instant,
    next_packet: u32,
    announced_size: Option<usize>,
    bytes: Vec<u8>,
    sender: oneshot::Sender<Option<Vec<u8>>>,
}

struct CleanupWorker {
    stop: Arc<(Mutex<bool>, Condvar)>,
    handle: JoinHandle<()>,
}

#[derive(Default)]
struct InventoryManagerEvents {
    find_object_by_path_reply: EventRegistry<FindObjectByPathReplyEventArgs>,
    folder_updated: EventRegistry<FolderUpdatedEventArgs>,
    inventory_object_offered: EventRegistry<InventoryObjectOfferedEventArgs>,
    item_received: EventRegistry<ItemReceivedEventArgs>,
    save_asset_to_inventory: EventRegistry<SaveAssetToInventoryEventArgs>,
    script_running_reply: EventRegistry<ScriptRunningReplyEventArgs>,
    task_inventory_reply: EventRegistry<TaskInventoryReplyEventArgs>,
    task_item_received: EventRegistry<TaskItemReceivedEventArgs>,
}

/// Shared state for a client-owned inventory manager.
pub(crate) struct InventoryManagerInner {
    client: ClientWeakHandle,
    store: RwLock<Option<Inventory>>,
    disposed: AtomicBool,
    callback_position: AtomicU32,
    request_position: AtomicU64,
    pending_created: Mutex<HashMap<u32, PendingCreated>>,
    pending_copied: Mutex<HashMap<u32, PendingCopy>>,
    pending_fetches: Mutex<HashMap<UUID, Vec<PendingFetch>>>,
    pending_task_replies: Mutex<HashMap<UUID, Vec<PendingTaskReply>>>,
    pending_offers: Mutex<Vec<PendingOffer>>,
    task_xfers: Mutex<HashMap<u64, TaskXfer>>,
    cleanup_worker: Mutex<Option<CleanupWorker>>,
    raw_packet_subscription: Mutex<Option<Subscription>>,
    caps_subscription: Mutex<Option<Subscription>>,
    im_subscription: Mutex<Option<Subscription>>,
    login_callback: Mutex<Option<NetworkManagerLoginResponseCallback>>,
    events: InventoryManagerEvents,
}

/// Stateful native replacement for the generated `InventoryManager` shell.
#[derive(Clone)]
pub struct InventoryManager {
    inner: Arc<InventoryManagerInner>,
}

impl fmt::Debug for InventoryManager {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("InventoryManager")
            .field("has_store", &read(&self.inner.store).is_some())
            .field("disposed", &self.inner.disposed.load(Ordering::Acquire))
            .finish()
    }
}

impl InventoryManager {
    /// Whether the manager's callback-expiry worker is still owned and may run.
    #[must_use]
    pub fn cleanup_worker_running(&self) -> bool {
        mutex(&self.inner.cleanup_worker).is_some()
    }

    /// Total asynchronous inventory operations still awaiting a reply.
    #[must_use]
    pub fn pending_operation_count(&self) -> usize {
        mutex(&self.inner.pending_created).len()
            + mutex(&self.inner.pending_copied).len()
            + mutex(&self.inner.pending_fetches)
                .values()
                .map(Vec::len)
                .sum::<usize>()
            + mutex(&self.inner.pending_task_replies)
                .values()
                .map(Vec::len)
                .sum::<usize>()
            + mutex(&self.inner.pending_offers).len()
            + mutex(&self.inner.task_xfers).len()
    }

    /// Whether deterministic manager teardown has completed.
    #[must_use]
    pub fn is_disposed(&self) -> bool {
        self.inner.disposed.load(Ordering::Acquire)
    }

    pub(crate) fn native_from_inner(inner: Arc<InventoryManagerInner>) -> Self {
        Self { inner }
    }

    pub(crate) fn native_inner(&self) -> Arc<InventoryManagerInner> {
        Arc::clone(&self.inner)
    }

    pub(crate) fn native_new(client: Option<Arc<GridClient>>) -> Result<Self, Error> {
        let client = client.ok_or(Error::ArgumentNull)?;
        let initial_store = Inventory::new_with_grid_client(Arc::clone(&client))?;
        let inner = Arc::new(InventoryManagerInner {
            client: client.native_weak_handle(),
            store: RwLock::new(Some(initial_store)),
            disposed: AtomicBool::new(false),
            callback_position: AtomicU32::new(0),
            request_position: AtomicU64::new(0),
            pending_created: Mutex::new(HashMap::new()),
            pending_copied: Mutex::new(HashMap::new()),
            pending_fetches: Mutex::new(HashMap::new()),
            pending_task_replies: Mutex::new(HashMap::new()),
            pending_offers: Mutex::new(Vec::new()),
            task_xfers: Mutex::new(HashMap::new()),
            cleanup_worker: Mutex::new(None),
            raw_packet_subscription: Mutex::new(None),
            caps_subscription: Mutex::new(None),
            im_subscription: Mutex::new(None),
            login_callback: Mutex::new(None),
            events: InventoryManagerEvents::default(),
        });
        let manager = Self { inner };
        manager.install_packet_handler()?;
        manager.install_message_handlers()?;
        manager.install_login_handler()?;
        manager.install_cleanup_worker()?;
        Ok(manager)
    }

    fn install_cleanup_worker(&self) -> Result<(), Error> {
        let stop = Arc::new((Mutex::new(false), Condvar::new()));
        let worker_stop = Arc::clone(&stop);
        let weak = Arc::downgrade(&self.inner);
        let handle = std::thread::Builder::new()
            .name("inventory-callback-cleanup".to_owned())
            .spawn(move || {
                loop {
                    let (lock, wake) = &*worker_stop;
                    let stopped = mutex(lock);
                    let (stopped, _) = wake
                        .wait_timeout_while(stopped, CALLBACK_SWEEP_INTERVAL, |value| !*value)
                        .unwrap_or_else(std::sync::PoisonError::into_inner);
                    if *stopped {
                        break;
                    }
                    drop(stopped);
                    let Some(inner) = weak.upgrade() else {
                        break;
                    };
                    InventoryManager::native_from_inner(inner).expire_pending(Instant::now());
                }
            })
            .map_err(|_| Error::InvalidOperation)?;
        *mutex(&self.inner.cleanup_worker) = Some(CleanupWorker { stop, handle });
        Ok(())
    }

    fn expire_pending(&self, now: Instant) {
        let created = {
            let mut pending = mutex(&self.inner.pending_created);
            let expired: Vec<_> = pending
                .iter()
                .filter_map(|(id, request)| (request.deadline <= now).then_some(*id))
                .collect();
            expired
                .into_iter()
                .filter_map(|id| pending.remove(&id))
                .collect::<Vec<_>>()
        };
        for request in created {
            let _ = request.sender.send(None);
        }
        let copied = {
            let mut pending = mutex(&self.inner.pending_copied);
            let expired: Vec<_> = pending
                .iter()
                .filter_map(|(id, request)| (request.deadline <= now).then_some(*id))
                .collect();
            expired
                .into_iter()
                .filter_map(|id| pending.remove(&id))
                .collect::<Vec<_>>()
        };
        for request in copied {
            let success = !request.copied.is_empty();
            let _ = request.sender.send(InventoryManagerCopyItemsResult {
                copied_items: success.then_some(request.copied),
                error: None,
                success,
            });
        }
        let fetches = expire_waiters(&self.inner.pending_fetches, now, |request| request.deadline);
        for request in fetches {
            let _ = request.sender.send(None);
        }
        let task_replies = expire_waiters(&self.inner.pending_task_replies, now, |request| {
            request.deadline
        });
        for request in task_replies {
            let _ = request.sender.send(String::new());
        }
        mutex(&self.inner.pending_offers).retain(|request| request.deadline > now);
        let xfers = {
            let mut pending = mutex(&self.inner.task_xfers);
            let expired: Vec<_> = pending
                .iter()
                .filter_map(|(id, request)| (request.deadline <= now).then_some(*id))
                .collect();
            expired
                .into_iter()
                .filter_map(|id| pending.remove(&id))
                .collect::<Vec<_>>()
        };
        for request in xfers {
            let _ = request.sender.send(None);
        }
    }

    fn client(&self) -> Result<GridClient, Error> {
        if self.inner.disposed.load(Ordering::Acquire) {
            return Err(Error::InvalidOperation);
        }
        self.inner.client.upgrade().ok_or(Error::InvalidOperation)
    }

    fn install_packet_handler(&self) -> Result<(), Error> {
        let network = self.client()?.native_network()?;
        let weak = Arc::downgrade(&self.inner);
        let subscription = network.subscribe_raw_packet(Arc::new(move |event| {
            if let Some(inner) = weak.upgrade() {
                let _ = InventoryManager::native_from_inner(inner).handle_raw_packet(event);
            }
        }));
        *mutex(&self.inner.raw_packet_subscription) = Some(subscription);
        Ok(())
    }

    fn install_message_handlers(&self) -> Result<(), Error> {
        let mut client = self.client()?;
        let weak = Arc::downgrade(&self.inner);
        let im_subscription = client.native_self().native_subscribe_im(Arc::new(
            move |event: InstantMessageEventArgs| {
                if let Some(inner) = weak.upgrade() {
                    let _ =
                        InventoryManager::native_from_inner(inner).handle_inventory_offer(event);
                }
            },
        ));
        *mutex(&self.inner.im_subscription) = Some(im_subscription);

        let weak = Arc::downgrade(&self.inner);
        let caps_subscription = client.native_network()?.subscribe_caps(
            "ScriptRunningReply".to_owned(),
            CapsEventQueueCallback::from_handler(move |_name, message, _simulator| {
                let Some(inner) = weak.upgrade() else {
                    return;
                };
                let Ok(mut reply) = ScriptRunningReplyMessage::new() else {
                    return;
                };
                if message
                    .serialize()
                    .and_then(|map| reply.deserialize(map))
                    .is_ok()
                {
                    inner
                        .events
                        .script_running_reply
                        .emit(ScriptRunningReplyEventArgs {
                            object_id: reply.object_id,
                            script_id: reply.item_id,
                            is_mono: reply.mono,
                            is_running: reply.running,
                        });
                }
            }),
        );
        *mutex(&self.inner.caps_subscription) = Some(caps_subscription);
        Ok(())
    }

    fn install_login_handler(&self) -> Result<(), Error> {
        let weak = Arc::downgrade(&self.inner);
        let callback = NetworkManagerLoginResponseCallback::from_handler(
            move |success, redirect, _message, _reason, response| {
                if success
                    && !redirect
                    && let (Some(inner), Some(response)) = (weak.upgrade(), response)
                {
                    let _ = InventoryManager::native_from_inner(inner)
                        .install_login_inventory(response);
                }
            },
        );
        self.client()?
            .native_network()?
            .native_register_login_response_callback(
                callback.clone(),
                Some(vec![
                    "inventory-root".to_owned(),
                    "inventory-skeleton".to_owned(),
                    "inventory-lib-root".to_owned(),
                    "inventory-lib-owner".to_owned(),
                    "inventory-skel-lib".to_owned(),
                ]),
            )?;
        *mutex(&self.inner.login_callback) = Some(callback);
        Ok(())
    }

    fn install_login_inventory(&self, response: LoginResponseData) -> Result<(), Error> {
        let client = Arc::new(self.client()?);
        let mut store = Inventory::new_with_grid_client_uuid(client, response.agent_id())?;
        let mut root = InventoryFolder::new(response.inventory_root())?;
        root.base.set_parent_uuid(UUID::zero());
        store.set_root_folder(Some(root));
        for folder in response.inventory_skeleton() {
            store.update_node_for(&folder)?;
        }
        let mut library = InventoryFolder::new(response.library_root())?;
        library.base.set_parent_uuid(UUID::zero());
        store.set_library_folder(Some(library));
        for folder in response.library_skeleton() {
            store.update_node_for(&folder)?;
        }
        *write(&self.inner.store) = Some(store);
        Ok(())
    }

    fn handle_raw_packet(&self, event: RawPacketReceivedEventArgs) -> Result<(), Error> {
        match event.packet_type {
            PacketType::UpdateCreateInventoryItem => {
                self.handle_update_create(decode_packet::<UpdateCreateInventoryItemPacket>(
                    &event.data,
                )?)?;
            }
            PacketType::BulkUpdateInventory => {
                self.handle_bulk_update(decode_packet::<BulkUpdateInventoryPacket>(&event.data)?)?;
            }
            PacketType::FetchInventoryReply => {
                self.handle_fetch_reply(decode_packet::<FetchInventoryReplyPacket>(&event.data)?)?;
            }
            PacketType::InventoryDescendents => {
                self.handle_descendents(decode_packet::<InventoryDescendentsPacket>(&event.data)?)?;
            }
            PacketType::MoveInventoryItem => {
                self.handle_move_item(decode_packet::<MoveInventoryItemPacket>(&event.data)?)?;
            }
            PacketType::SaveAssetIntoInventory => {
                let packet = decode_packet::<SaveAssetIntoInventoryPacket>(&event.data)?;
                self.inner
                    .events
                    .save_asset_to_inventory
                    .emit(SaveAssetToInventoryEventArgs {
                        item_id: packet.inventory_data.item_id,
                        new_asset_id: packet.inventory_data.new_asset_id,
                    });
            }
            PacketType::ReplyTaskInventory => {
                let packet = decode_packet::<ReplyTaskInventoryPacket>(&event.data)?;
                let filename = wire_string(packet.inventory_data.filename);
                let waiters = mutex(&self.inner.pending_task_replies)
                    .remove(&packet.inventory_data.task_id)
                    .unwrap_or_default();
                for waiter in waiters {
                    let _ = waiter.sender.send(filename.clone());
                }
                self.inner
                    .events
                    .task_inventory_reply
                    .emit(TaskInventoryReplyEventArgs {
                        item_id: packet.inventory_data.task_id,
                        serial: packet.inventory_data.serial,
                        asset_filename: filename,
                    });
            }
            PacketType::SendXferPacket => self.handle_task_xfer_packet(
                decode_packet::<SendXferPacketPacket>(&event.data)?,
                event.simulator,
            )?,
            _ => {}
        }
        Ok(())
    }

    fn handle_inventory_offer(&self, event: InstantMessageEventArgs) -> Result<(), Error> {
        let offer = event.im();
        let (asset_type, object_id, from_task) = match offer.dialog {
            InstantMessageDialog::InventoryOffered if offer.binary_bucket.len() == 17 => (
                asset_type_from_i8(offer.binary_bucket[0].cast_signed()),
                UUID::from_bytes(offer.binary_bucket.clone(), 1)?,
                false,
            ),
            InstantMessageDialog::TaskInventoryOffered if offer.binary_bucket.len() == 1 => (
                asset_type_from_i8(offer.binary_bucket[0].cast_signed()),
                UUID::zero(),
                true,
            ),
            InstantMessageDialog::InventoryOffered | InstantMessageDialog::TaskInventoryOffered => {
                return Ok(());
            }
            _ => return Ok(()),
        };
        let args = InventoryObjectOfferedEventArgs::new(
            offer.clone(),
            asset_type,
            object_id,
            from_task,
            self.native_find_folder_for_asset_type(asset_type),
        )?;
        self.inner
            .events
            .inventory_object_offered
            .emit(args.clone());
        let waiters = std::mem::take(&mut *mutex(&self.inner.pending_offers));
        for waiter in waiters {
            let _ = waiter.sender.send(args.clone());
        }

        let response_dialog = match (offer.dialog, args.accept()) {
            (InstantMessageDialog::InventoryOffered, true) => {
                InstantMessageDialog::InventoryAccepted
            }
            (InstantMessageDialog::TaskInventoryOffered, true) => {
                InstantMessageDialog::TaskInventoryAccepted
            }
            (InstantMessageDialog::InventoryOffered, false) => {
                InstantMessageDialog::InventoryDeclined
            }
            (InstantMessageDialog::TaskInventoryOffered, false) => {
                InstantMessageDialog::TaskInventoryDeclined
            }
            _ => return Ok(()),
        };
        let bucket = if args.accept() {
            args.folder_id().get_bytes()?
        } else {
            Vec::new()
        };
        let mut client = self.client()?;
        let agent = client.native_self();
        let _ = agent.native_instant_message_full(
            agent.native_name(),
            offer.from_agent_id,
            String::new(),
            offer.im_session_id,
            response_dialog,
            InstantMessageOnline::Online,
            Vector3::zero(),
            UUID::zero(),
            bucket,
        );
        if args.accept() && object_id != UUID::zero() {
            let _ = self.native_request_fetch_inventory(
                HashMap::from([(object_id, offer.to_agent_id)]),
                None,
            );
        }
        Ok(())
    }

    fn handle_task_xfer_packet(
        &self,
        packet: SendXferPacketPacket,
        simulator: Simulator,
    ) -> Result<(), Error> {
        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 complete = None;
        let confirmation;
        {
            let mut transfers = mutex(&self.inner.task_xfers);
            let Some(transfer) = transfers.get_mut(&xfer_id) else {
                return Ok(());
            };
            if packet_number == transfer.next_packet {
                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_TASK_INVENTORY_BYTES {
                        let transfer = transfers.remove(&xfer_id).expect("known xfer");
                        drop(transfers);
                        let _ = transfer.sender.send(None);
                        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 {
                    let transfer = transfers.remove(&xfer_id).expect("known xfer");
                    drop(transfers);
                    let _ = transfer.sender.send(None);
                    return Err(Error::InvalidOperation);
                }
                transfer.bytes.extend_from_slice(&incoming);
                transfer.next_packet = transfer.next_packet.saturating_add(1);
                if is_last {
                    let transfer = transfers.remove(&xfer_id).expect("known xfer");
                    if transfer.bytes.len() == size {
                        complete = Some((transfer.sender, Some(transfer.bytes)));
                    } else {
                        complete = Some((transfer.sender, None));
                    }
                }
            } else {
                confirmation = transfer.next_packet.saturating_sub(1);
            }
        }
        let mut confirm = ConfirmXferPacketPacket::new_with_constructor()?;
        confirm.xfer_id.id = xfer_id;
        confirm.xfer_id.packet = confirmation;
        let confirmation_result =
            self.send_generated(&simulator, PacketType::ConfirmXferPacket, &confirm);
        if let Some((sender, data)) = complete {
            let _ = sender.send(data);
        }
        confirmation_result
    }

    fn store_clone(&self) -> Option<Inventory> {
        read(&self.inner.store).clone()
    }

    pub(crate) fn native_reconcile_ais(
        &self,
        folders: Vec<InventoryFolder>,
        items: Vec<InventoryItem>,
        meta: &crate::inventory_ais::AISResponseMeta,
    ) -> Result<(), Error> {
        let Some(store) = self.store_clone() else {
            return Ok(());
        };
        store.native_apply_ais_batch(
            folders,
            items,
            &meta.items_removed(),
            &meta.categories_removed(),
            &meta.broken_links_removed(),
            &meta.category_version_updates(),
        )
    }

    #[cfg(test)]
    pub(crate) fn native_set_store_for_tests(&self, store: Inventory) {
        *write(&self.inner.store) = Some(store);
    }

    fn cache_server_item(&self, item: &InventoryItem) -> Result<(), Error> {
        if item.base.uuid() == UUID::zero() || item.inventory_type() == InventoryType::FOLDER {
            return Err(Error::Argument);
        }
        if let Some(store) = self.store_clone() {
            let typed = typed_inventory_item(item.clone());
            store.update_node_for(typed.as_ref())?;
        }
        Ok(())
    }

    fn complete_created_item(&self, callback_id: u32, item: InventoryItem) {
        let created = mutex(&self.inner.pending_created).remove(&callback_id);
        if let Some(created) = created {
            let _ = created.sender.send(Some(item.clone()));
        }
        let mut copied = mutex(&self.inner.pending_copied);
        let Some(copy) = copied.get_mut(&callback_id) else {
            return;
        };
        copy.copied.push(item.base.clone());
        if copy.copied.len() < copy.expected {
            return;
        }
        let copy = copied
            .remove(&callback_id)
            .expect("copy request still present");
        drop(copied);
        let _ = copy.sender.send(InventoryManagerCopyItemsResult {
            copied_items: Some(copy.copied),
            error: None,
            success: true,
        });
    }

    fn handle_update_create(&self, packet: UpdateCreateInventoryItemPacket) -> Result<(), Error> {
        for block in packet.inventory_data {
            if block.item_id == UUID::zero() || block.inv_type == InventoryType::FOLDER.0 {
                continue;
            }
            let mut item = inventory_item_from_wire(InventoryWireData {
                asset_id: block.asset_id,
                base_mask: block.base_mask,
                creation_date: block.creation_date,
                creator_id: block.creator_id,
                description: block.description,
                everyone_mask: block.everyone_mask,
                flags: block.flags,
                folder_id: block.folder_id,
                group_id: block.group_id,
                group_mask: block.group_mask,
                group_owned: block.group_owned,
                inv_type: block.inv_type,
                item_id: block.item_id,
                name: block.name,
                next_owner_mask: block.next_owner_mask,
                owner_id: block.owner_id,
                owner_mask: block.owner_mask,
                sale_price: block.sale_price,
                sale_type: block.sale_type,
                type_: block.type_,
            })?;
            if item.base.parent_uuid() == UUID::zero() {
                item.base
                    .set_parent_uuid(self.native_find_folder_for_asset_type(item.asset_type()));
                self.native_request_update_items(vec![item.clone()], UUID::zero())?;
            }
            self.cache_server_item(&item)?;
            self.complete_created_item(block.callback_id, item.clone());
            self.inner
                .events
                .task_item_received
                .emit(TaskItemReceivedEventArgs {
                    item_id: item.base.uuid(),
                    folder_id: item.base.parent_uuid(),
                    creator_id: item.creator_id(),
                    asset_id: item.asset_uuid(),
                    type_: item.inventory_type(),
                });
        }
        Ok(())
    }

    fn handle_bulk_update(&self, packet: BulkUpdateInventoryPacket) -> Result<(), Error> {
        let store = self.store_clone();
        if let Some(store) = &store {
            for block in packet.folder_data {
                if block.folder_id == UUID::zero() {
                    continue;
                }
                let mut folder = store
                    .get_value_or_default_with_uuid_a7c63fbe::<InventoryFolder>(block.folder_id)?
                    .unwrap_or(InventoryFolder::new(block.folder_id)?);
                folder.base.set_name(wire_string(block.name));
                folder.base.set_owner_id(packet.agent_data.agent_id);
                folder.base.set_parent_uuid(block.parent_id);
                folder.set_preferred_type(folder_type_from_i8(block.type_));
                store.update_node_for(&folder)?;
            }
        }
        for block in packet.item_data {
            if block.item_id == UUID::zero() {
                continue;
            }
            let item = inventory_item_from_wire(InventoryWireData {
                asset_id: block.asset_id,
                base_mask: block.base_mask,
                creation_date: block.creation_date,
                creator_id: block.creator_id,
                description: block.description,
                everyone_mask: block.everyone_mask,
                flags: block.flags,
                folder_id: block.folder_id,
                group_id: block.group_id,
                group_mask: block.group_mask,
                group_owned: block.group_owned,
                inv_type: block.inv_type,
                item_id: block.item_id,
                name: block.name,
                next_owner_mask: block.next_owner_mask,
                owner_id: block.owner_id,
                owner_mask: block.owner_mask,
                sale_price: block.sale_price,
                sale_type: block.sale_type,
                type_: block.type_,
            })?;
            self.cache_server_item(&item)?;
            self.complete_created_item(block.callback_id, item);
        }
        Ok(())
    }

    fn handle_fetch_reply(&self, packet: FetchInventoryReplyPacket) -> Result<(), Error> {
        for block in packet.inventory_data {
            if block.item_id == UUID::zero() || block.inv_type == InventoryType::FOLDER.0 {
                continue;
            }
            let item = inventory_item_from_wire(InventoryWireData {
                asset_id: block.asset_id,
                base_mask: block.base_mask,
                creation_date: block.creation_date,
                creator_id: block.creator_id,
                description: block.description,
                everyone_mask: block.everyone_mask,
                flags: block.flags,
                folder_id: block.folder_id,
                group_id: block.group_id,
                group_mask: block.group_mask,
                group_owned: block.group_owned,
                inv_type: block.inv_type,
                item_id: block.item_id,
                name: block.name,
                next_owner_mask: block.next_owner_mask,
                owner_id: block.owner_id,
                owner_mask: block.owner_mask,
                sale_price: block.sale_price,
                sale_type: block.sale_type,
                type_: block.type_,
            })?;
            self.cache_server_item(&item)?;
            let waiters = mutex(&self.inner.pending_fetches)
                .remove(&item.base.uuid())
                .unwrap_or_default();
            for waiter in waiters {
                let _ = waiter.sender.send(Some(item.clone()));
            }
            self.inner
                .events
                .item_received
                .emit(ItemReceivedEventArgs { item });
        }
        Ok(())
    }

    fn handle_descendents(&self, packet: InventoryDescendentsPacket) -> Result<(), Error> {
        let folder_id = packet.agent_data.folder_id;
        let owner_id = packet.agent_data.owner_id;
        let Some(store) = self.store_clone() else {
            return Ok(());
        };
        let mut parent = store
            .get_value_or_default_with_uuid_a7c63fbe::<InventoryFolder>(folder_id)?
            .unwrap_or(InventoryFolder::new(folder_id)?);
        if packet.agent_data.version < parent.version() {
            return Ok(());
        }
        for block in packet.folder_data {
            if block.folder_id == UUID::zero() {
                continue;
            }
            let mut folder = InventoryFolder::new(block.folder_id)?;
            folder.base.set_parent_uuid(block.parent_id);
            folder.base.set_owner_id(owner_id);
            folder.base.set_name(wire_string(block.name));
            folder.set_preferred_type(folder_type_from_i8(block.type_));
            store.update_node_for(&folder)?;
        }
        for block in packet.item_data {
            if block.item_id == UUID::zero() {
                continue;
            }
            let item = inventory_item_from_wire(InventoryWireData {
                asset_id: block.asset_id,
                base_mask: block.base_mask,
                creation_date: block.creation_date,
                creator_id: block.creator_id,
                description: block.description,
                everyone_mask: block.everyone_mask,
                flags: block.flags,
                folder_id: block.folder_id,
                group_id: block.group_id,
                group_mask: block.group_mask,
                group_owned: block.group_owned,
                inv_type: block.inv_type,
                item_id: block.item_id,
                name: block.name,
                next_owner_mask: block.next_owner_mask,
                owner_id,
                owner_mask: block.owner_mask,
                sale_price: block.sale_price,
                sale_type: block.sale_type,
                type_: block.type_,
            })?;
            self.cache_server_item(&item)?;
        }
        parent.set_version(packet.agent_data.version);
        parent.set_descendent_count(packet.agent_data.descendents);
        parent.base.set_owner_id(owner_id);
        store.update_node_for(&parent)?;
        self.inner
            .events
            .folder_updated
            .emit(FolderUpdatedEventArgs {
                folder_id,
                success: true,
            });
        Ok(())
    }

    fn handle_move_item(&self, packet: MoveInventoryItemPacket) -> Result<(), Error> {
        let Some(store) = self.store_clone() else {
            return Ok(());
        };
        for block in packet.inventory_data {
            let Some(mut item) = store.native_item_value(block.item_id) else {
                continue;
            };
            item.base.set_parent_uuid(block.folder_id);
            let name = wire_string(block.new_name);
            if !name.is_empty() {
                item.base.set_name(name);
            }
            store.update_node_for(&item)?;
        }
        Ok(())
    }

    pub(crate) fn native_subscribe_find_object_by_path_reply(
        &self,
        handler: EventHandler<FindObjectByPathReplyEventArgs>,
    ) -> Subscription {
        self.inner
            .events
            .find_object_by_path_reply
            .subscribe(handler)
    }
    pub(crate) fn native_subscribe_folder_updated(
        &self,
        handler: EventHandler<FolderUpdatedEventArgs>,
    ) -> Subscription {
        self.inner.events.folder_updated.subscribe(handler)
    }
    pub(crate) fn native_subscribe_inventory_object_offered(
        &self,
        handler: EventHandler<InventoryObjectOfferedEventArgs>,
    ) -> Subscription {
        self.inner
            .events
            .inventory_object_offered
            .subscribe(handler)
    }
    pub(crate) fn native_subscribe_item_received(
        &self,
        handler: EventHandler<ItemReceivedEventArgs>,
    ) -> Subscription {
        self.inner.events.item_received.subscribe(handler)
    }
    pub(crate) fn native_subscribe_save_asset_to_inventory(
        &self,
        handler: EventHandler<SaveAssetToInventoryEventArgs>,
    ) -> Subscription {
        self.inner.events.save_asset_to_inventory.subscribe(handler)
    }
    pub(crate) fn native_subscribe_script_running_reply(
        &self,
        handler: EventHandler<ScriptRunningReplyEventArgs>,
    ) -> Subscription {
        self.inner.events.script_running_reply.subscribe(handler)
    }
    pub(crate) fn native_subscribe_task_inventory_reply(
        &self,
        handler: EventHandler<TaskInventoryReplyEventArgs>,
    ) -> Subscription {
        self.inner.events.task_inventory_reply.subscribe(handler)
    }
    pub(crate) fn native_subscribe_task_item_received(
        &self,
        handler: EventHandler<TaskItemReceivedEventArgs>,
    ) -> Subscription {
        self.inner.events.task_item_received.subscribe(handler)
    }

    pub(crate) fn native_magic_id() -> UUID {
        UUID::new_with_string(MAGIC_ID_TEXT.to_owned()).expect("static inventory magic UUID")
    }

    pub(crate) fn native_decrypt_shadow_id(shadow_id: UUID) -> Result<UUID, Error> {
        Ok(UUID::bitxor(shadow_id, Self::native_magic_id()))
    }

    pub(crate) fn native_encrypt_asset_id(asset_id: UUID) -> Result<UUID, Error> {
        Self::native_decrypt_shadow_id(asset_id)
    }

    pub(crate) fn native_item_crc(item: &InventoryItem) -> Result<u32, Error> {
        let mut crc = 0_u32;
        for value in [
            item.asset_uuid(),
            item.base.parent_uuid(),
            item.base.uuid(),
            item.creator_id(),
            item.base.owner_id(),
            item.group_id(),
        ] {
            crc = crc.wrapping_add(uuid_crc(value));
        }
        let permissions = item.permissions();
        for value in [
            permissions.owner_mask.0,
            permissions.next_owner_mask.0,
            permissions.everyone_mask.0,
            permissions.group_mask.0,
            item.flags(),
            i32::from(item.inventory_type().0).cast_unsigned(),
            i32::from(item.asset_type() as i8).cast_unsigned(),
            unix_seconds_u32_wrapping(item.creation_date()),
            item.sale_price().cast_unsigned(),
            u32::from(item.sale_type() as u8).wrapping_mul(0x0707_3096),
        ] {
            crc = crc.wrapping_add(value);
        }
        Ok(crc)
    }

    pub(crate) fn native_create_inventory_item(
        type_: InventoryType,
        id: UUID,
    ) -> Result<Box<dyn InventoryItemClass>, Error> {
        macro_rules! boxed {
            ($kind:ty) => {
                Box::new(<$kind>::new(id)?) as Box<dyn InventoryItemClass>
            };
        }
        Ok(match type_ {
            InventoryType::TEXTURE => boxed!(InventoryTexture),
            InventoryType::SOUND => boxed!(InventorySound),
            InventoryType::CALLING_CARD => boxed!(InventoryCallingCard),
            InventoryType::LANDMARK => boxed!(InventoryLandmark),
            InventoryType::OBJECT => boxed!(InventoryObject),
            InventoryType::NOTECARD => boxed!(InventoryNotecard),
            InventoryType::CATEGORY => boxed!(InventoryCategory),
            InventoryType::LSL => boxed!(InventoryLSL),
            InventoryType::SNAPSHOT => boxed!(InventorySnapshot),
            InventoryType::ATTACHMENT => boxed!(InventoryAttachment),
            InventoryType::WEARABLE => boxed!(InventoryWearable),
            InventoryType::ANIMATION => boxed!(InventoryAnimation),
            InventoryType::GESTURE => boxed!(InventoryGesture),
            InventoryType::SETTINGS => boxed!(InventorySettings),
            InventoryType::MATERIAL => boxed!(InventoryMaterial),
            _ => Box::new(InventoryItem::new_with_inventory_type_uuid(type_, id)?),
        })
    }

    pub(crate) fn native_store(&self) -> Option<Inventory> {
        read(&self.inner.store).clone()
    }

    pub(crate) fn native_no_results() -> Task<Vec<InventoryBase>> {
        Task::ready(Vec::new())
    }

    pub(crate) fn native_create_or_retrieve_inventory_item(
        &self,
        inventory_type: InventoryType,
        item_id: UUID,
    ) -> Result<InventoryItem, Error> {
        if let Some(item) = self
            .store_clone()
            .and_then(|store| store.native_item_value(item_id))
        {
            return Ok(item);
        }
        InventoryItem::new_with_inventory_type_uuid(inventory_type, item_id)
    }

    pub(crate) fn native_create_folder(
        &self,
        parent_id: UUID,
        mut name: String,
        preferred_type: FolderType,
    ) -> Result<UUID, Error> {
        let store = self.store_clone();
        if preferred_type != FolderType::None {
            let owner_id = self.ids()?.0;
            if let Some(existing) = store
                .as_ref()
                .and_then(|store| store.find_folder_for_type(preferred_type))
                .and_then(|folder_id| {
                    store.as_ref().and_then(|store| {
                        store
                            .get_value_or_default_with_uuid_a7c63fbe::<InventoryFolder>(folder_id)
                            .ok()
                            .flatten()
                    })
                })
                .filter(|folder| folder.base.owner_id() == owner_id)
            {
                return Ok(existing.base.uuid());
            }
        }
        if name.is_empty() {
            preferred_type
                .enum_text()
                .unwrap_or("New Folder")
                .clone_into(&mut name);
        }
        let folder_id = UUID::random()?;
        let owner_id = self.ids()?.0;
        let mut folder = InventoryFolder::new(folder_id)?;
        folder.base.set_parent_uuid(parent_id);
        folder.base.set_owner_id(owner_id);
        folder.base.set_name(name.clone());
        folder.set_version(1);
        folder.set_descendent_count(0);
        folder.set_preferred_type(preferred_type);
        if let Some(store) = store {
            store.update_node_for(&folder)?;
        }

        let simulator = self.current_simulator(None)?;
        let (agent_id, session_id) = self.ids()?;
        let mut packet = CreateInventoryFolderPacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent_id;
        packet.agent_data.session_id = session_id;
        packet.folder_data.folder_id = folder_id;
        packet.folder_data.parent_id = parent_id;
        packet.folder_data.type_ = preferred_type as i8;
        packet.folder_data.name = Utils::string_to_bytes(name)?;
        self.send_generated(&simulator, PacketType::CreateInventoryFolder, &packet)?;
        Ok(folder_id)
    }

    #[allow(clippy::too_many_arguments)]
    pub(crate) async fn native_create_item(
        &self,
        parent_folder: UUID,
        name: String,
        description: String,
        asset_type: AssetType,
        asset_transaction_id: UUID,
        inventory_type: InventoryType,
        wearable_type: WearableType,
        next_owner_mask: PermissionMask,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<Option<InventoryItem>, Error> {
        let cancellation_token = cancellation_token.unwrap_or_default();
        cancellation_token.throw_if_cancellation_requested()?;
        self.ensure_pending_capacity()?;
        let callback_id = self.next_callback_id();
        let (sender, receiver) = oneshot::channel();
        mutex(&self.inner.pending_created).insert(
            callback_id,
            PendingCreated {
                deadline: Instant::now() + CALLBACK_TIMEOUT,
                sender,
            },
        );
        let send_result = (|| {
            let simulator = self.current_simulator(None)?;
            let (agent_id, session_id) = self.ids()?;
            let mut packet = CreateInventoryItemPacket::new_with_constructor()?;
            packet.agent_data.agent_id = agent_id;
            packet.agent_data.session_id = session_id;
            packet.inventory_block.callback_id = callback_id;
            packet.inventory_block.folder_id = parent_folder;
            packet.inventory_block.transaction_id = asset_transaction_id;
            packet.inventory_block.next_owner_mask = next_owner_mask.0;
            packet.inventory_block.type_ = asset_type as i8;
            packet.inventory_block.inv_type = inventory_type.0;
            packet.inventory_block.wearable_type = wearable_type as u8;
            packet.inventory_block.name = Utils::string_to_bytes(name)?;
            packet.inventory_block.description = Utils::string_to_bytes(description)?;
            self.send_generated(&simulator, PacketType::CreateInventoryItem, &packet)
        })();
        if let Err(error) = send_result {
            mutex(&self.inner.pending_created).remove(&callback_id);
            return Err(error);
        }
        let cancelled = cancellation_token.cancelled();
        futures_util::pin_mut!(receiver, cancelled);
        match select(receiver, cancelled).await {
            Either::Left((Ok(item), _)) => Ok(item),
            Either::Left((Err(_), _)) => Ok(None),
            Either::Right(_) => {
                mutex(&self.inner.pending_created).remove(&callback_id);
                Err(Error::Cancelled)
            }
        }
    }

    #[allow(clippy::too_many_arguments)]
    pub(crate) async fn native_create_link(
        &self,
        folder_id: UUID,
        item_id: UUID,
        name: String,
        description: String,
        inventory_type: InventoryType,
        transaction_id: UUID,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<Option<InventoryItem>, Error> {
        let cancellation_token = cancellation_token.unwrap_or_default();
        cancellation_token.throw_if_cancellation_requested()?;
        if let Ok(simulator) = self.current_simulator(None)
            && self
                .wait_for_capability(&simulator, "InventoryAPIv3", &cancellation_token)
                .await?
                .is_some()
        {
            let mut created = self
                .create_links_via_cap(
                    folder_id,
                    vec![(item_id, name, description, inventory_type)],
                    cancellation_token,
                )
                .await?;
            return Ok(created.pop());
        }

        self.ensure_pending_capacity()?;
        let callback_id = self.next_callback_id();
        let (sender, receiver) = oneshot::channel();
        mutex(&self.inner.pending_created).insert(
            callback_id,
            PendingCreated {
                deadline: Instant::now() + CALLBACK_TIMEOUT,
                sender,
            },
        );
        let send_result = (|| {
            let simulator = self.current_simulator(None)?;
            let (agent_id, session_id) = self.ids()?;
            let mut packet = LinkInventoryItemPacket::new_with_constructor()?;
            packet.agent_data.agent_id = agent_id;
            packet.agent_data.session_id = session_id;
            packet.inventory_block.callback_id = callback_id;
            packet.inventory_block.folder_id = folder_id;
            packet.inventory_block.transaction_id = transaction_id;
            packet.inventory_block.old_item_id = item_id;
            packet.inventory_block.type_ = if inventory_type == InventoryType::FOLDER {
                AssetType::LinkFolder as i8
            } else {
                AssetType::Link as i8
            };
            packet.inventory_block.inv_type = inventory_type.0;
            packet.inventory_block.name = Utils::string_to_bytes(name)?;
            packet.inventory_block.description = Utils::string_to_bytes(description)?;
            self.send_generated(&simulator, PacketType::LinkInventoryItem, &packet)
        })();
        if let Err(error) = send_result {
            mutex(&self.inner.pending_created).remove(&callback_id);
            return Err(error);
        }
        let cancelled = cancellation_token.cancelled();
        futures_util::pin_mut!(receiver, cancelled);
        match select(receiver, cancelled).await {
            Either::Left((Ok(item), _)) => Ok(item),
            Either::Left((Err(_), _)) => Ok(None),
            Either::Right(_) => {
                mutex(&self.inner.pending_created).remove(&callback_id);
                Err(Error::Cancelled)
            }
        }
    }

    pub(crate) async fn native_create_links(
        &self,
        folder_id: UUID,
        links: Box<dyn Iterator<Item = (InventoryItem, String)>>,
        callback: Option<Box<dyn Fn(bool) + Send + Sync>>,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<(), Error> {
        let cancellation_token = cancellation_token.unwrap_or_default();
        cancellation_token.throw_if_cancellation_requested()?;
        let links: Vec<_> = links.collect();
        if links.is_empty() {
            if let Some(callback) = callback {
                callback(true);
            }
            return Ok(());
        }
        let cap = if let Ok(simulator) = self.current_simulator(None) {
            self.wait_for_capability(&simulator, "InventoryAPIv3", &cancellation_token)
                .await?
        } else {
            None
        };
        if cap.is_some() {
            let values = links
                .into_iter()
                .map(|(item, description)| {
                    (
                        item.base.uuid(),
                        item.base.name(),
                        description,
                        item.inventory_type(),
                    )
                })
                .collect();
            let success = self
                .create_links_via_cap(folder_id, values, cancellation_token)
                .await
                .is_ok();
            if let Some(callback) = callback {
                callback(success);
            }
            return Ok(());
        }
        let mut success = true;
        for (item, description) in links {
            cancellation_token.throw_if_cancellation_requested()?;
            if self
                .native_create_link(
                    folder_id,
                    item.base.uuid(),
                    item.base.name(),
                    description,
                    item.inventory_type(),
                    UUID::random()?,
                    Some(cancellation_token.clone()),
                )
                .await?
                .is_none()
            {
                success = false;
            }
        }
        if let Some(callback) = callback {
            callback(success);
        }
        Ok(())
    }

    async fn create_links_via_cap(
        &self,
        folder_id: UUID,
        links: Vec<(UUID, String, String, InventoryType)>,
        cancellation_token: CancellationToken,
    ) -> Result<Vec<InventoryItem>, Error> {
        let simulator = self.current_simulator(None)?;
        let cap = self
            .wait_for_capability(&simulator, "InventoryAPIv3", &cancellation_token)
            .await?
            .ok_or(Error::InvalidOperation)?;
        let uri = Uri(format!(
            "{}/category/{}",
            cap.0.trim_end_matches('/'),
            folder_id
        ));
        let payload = OSD::Map(HashMap::from([(
            "links".to_owned(),
            OSD::Array(
                links
                    .into_iter()
                    .map(|(linked_id, name, description, inventory_type)| {
                        OSD::Map(HashMap::from([
                            ("linked_id".to_owned(), OSD::UUID(linked_id)),
                            ("name".to_owned(), OSD::String(name)),
                            ("desc".to_owned(), OSD::String(description)),
                            (
                                "type".to_owned(),
                                OSD::Integer(if inventory_type == InventoryType::FOLDER {
                                    AssetType::LinkFolder as i32
                                } else {
                                    AssetType::Link as i32
                                }),
                            ),
                            (
                                "inv_type".to_owned(),
                                OSD::Integer(i32::from(inventory_type.0)),
                            ),
                        ]))
                    })
                    .collect(),
            ),
        )]));
        let bytes = OSDParser::serialize_llsd_xml_bytes(payload)?;
        let (response, body) = self
            .client()?
            .native_http_caps_client()
            .post_with_uri_string_bytes_cancellation_token_i_progress(
                uri,
                "application/llsd+xml".to_owned(),
                bytes,
                cancellation_token,
                None,
            )
            .await?;
        if !response.is_success_status_code() {
            return Err(Error::HttpRequest);
        }
        let mut created = Vec::new();
        if let Ok(OSD::Map(root)) = OSDParser::deserialize_with_bytes(body)
            && let Some(OSD::Map(embedded)) = root.get("_embedded")
            && let Some(OSD::Map(values)) = embedded.get("links")
        {
            for value in values.values() {
                if let Ok(item) = InventoryItem::from_osd(value.clone()) {
                    self.cache_server_item(&item)?;
                    self.inner
                        .events
                        .item_received
                        .emit(ItemReceivedEventArgs { item: item.clone() });
                    created.push(item);
                }
            }
        }
        Ok(created)
    }

    pub(crate) fn native_request_fetch_inventory(
        &self,
        items: HashMap<UUID, UUID>,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<(), Error> {
        cancellation_token
            .unwrap_or_default()
            .throw_if_cancellation_requested()?;
        let simulator = self.current_simulator(None)?;
        let (agent_id, session_id) = self.ids()?;
        let mut packet = FetchInventoryPacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent_id;
        packet.agent_data.session_id = session_id;
        packet.inventory_data = items
            .into_iter()
            .map(|(item_id, owner_id)| {
                let mut block = FetchInventoryPacketInventoryDataBlock::new_with_constructor()?;
                block.item_id = item_id;
                block.owner_id = owner_id;
                Ok(block)
            })
            .collect::<Result<Vec<_>, Error>>()?;
        self.send_generated(&simulator, PacketType::FetchInventory, &packet)
    }

    pub(crate) async fn native_request_fetch_inventory_async(
        &self,
        items: HashMap<UUID, UUID>,
        cancellation_token: Option<CancellationToken>,
        callback: Option<Box<dyn Fn(Vec<InventoryItem>) + Send + Sync>>,
    ) -> Result<(), Error> {
        let cancellation_token = cancellation_token.unwrap_or_default();
        cancellation_token.throw_if_cancellation_requested()?;
        let capability = if let Ok(simulator) = self.current_simulator(None) {
            self.wait_for_capability(&simulator, "FetchInventory2", &cancellation_token)
                .await?
        } else {
            None
        };
        if let Some(capability) = capability {
            let fetched = self
                .fetch_inventory_via_cap(capability, items, cancellation_token)
                .await?;
            if let Some(callback) = callback {
                callback(fetched);
            }
            return Ok(());
        }
        self.native_request_fetch_inventory(items, Some(cancellation_token))
    }

    async fn fetch_inventory_via_cap(
        &self,
        capability: Uri,
        items: HashMap<UUID, UUID>,
        cancellation_token: CancellationToken,
    ) -> Result<Vec<InventoryItem>, Error> {
        let agent_id = self.ids()?.0;
        let item_values = items
            .into_iter()
            .map(|(item_id, owner_id)| {
                OSD::Map(HashMap::from([
                    ("item_id".to_owned(), OSD::UUID(item_id)),
                    ("owner_id".to_owned(), OSD::UUID(owner_id)),
                ]))
            })
            .collect();
        let payload = OSD::Map(HashMap::from([
            ("agent_id".to_owned(), OSD::UUID(agent_id)),
            ("items".to_owned(), OSD::Array(item_values)),
        ]));
        let body = OSDParser::serialize_llsd_xml_bytes(payload)?;
        let (response, response_body) = self
            .client()?
            .native_http_caps_client()
            .post_with_uri_string_bytes_cancellation_token_i_progress(
                capability,
                "application/llsd+xml".to_owned(),
                body,
                cancellation_token,
                None,
            )
            .await?;
        if !response.is_success_status_code() {
            return Err(Error::HttpRequest);
        }
        let OSD::Map(map) = OSDParser::deserialize_with_bytes(response_body)? else {
            return Err(Error::Argument);
        };
        let Some(OSD::Array(values)) = map.get("items") else {
            return Ok(Vec::new());
        };
        let mut fetched = Vec::with_capacity(values.len());
        for value in values {
            let item = InventoryItem::from_osd(value.clone())?;
            if item.base.uuid() == UUID::zero() {
                continue;
            }
            self.cache_server_item(&item)?;
            let waiters = mutex(&self.inner.pending_fetches)
                .remove(&item.base.uuid())
                .unwrap_or_default();
            for waiter in waiters {
                let _ = waiter.sender.send(Some(item.clone()));
            }
            self.inner
                .events
                .item_received
                .emit(ItemReceivedEventArgs { item: item.clone() });
            fetched.push(item);
        }
        Ok(fetched)
    }

    pub(crate) async fn native_fetch_item(
        &self,
        item_id: UUID,
        owner_id: UUID,
        cancellation_token: Option<CancellationToken>,
        force_http: bool,
    ) -> Result<Option<InventoryItem>, Error> {
        let cancellation_token = cancellation_token.unwrap_or_default();
        if cancellation_token.is_cancellation_requested() {
            return Ok(None);
        }
        self.ensure_pending_capacity()?;
        let request_id = self.next_request_id();
        let (sender, receiver) = oneshot::channel();
        mutex(&self.inner.pending_fetches)
            .entry(item_id)
            .or_default()
            .push(PendingFetch {
                request_id,
                deadline: Instant::now() + CALLBACK_TIMEOUT,
                sender,
            });

        let request = HashMap::from([(item_id, owner_id)]);
        let send_result = if force_http {
            let capability = if let Ok(simulator) = self.current_simulator(None) {
                self.wait_for_capability(&simulator, "FetchInventory2", &cancellation_token)
                    .await?
            } else {
                None
            };
            if let Some(capability) = capability {
                self.fetch_inventory_via_cap(capability, request, cancellation_token.clone())
                    .await
                    .map(|_| ())
            } else {
                self.remove_pending_fetch(item_id, request_id);
                return Ok(None);
            }
        } else {
            self.native_request_fetch_inventory_async(
                request,
                Some(cancellation_token.clone()),
                None,
            )
            .await
        };
        if let Err(error) = send_result {
            self.remove_pending_fetch(item_id, request_id);
            return Err(error);
        }

        let cancelled = cancellation_token.cancelled();
        futures_util::pin_mut!(receiver, cancelled);
        match select(receiver, cancelled).await {
            Either::Left((Ok(item), _)) => Ok(item),
            Either::Left((Err(_), _)) | Either::Right(_) => {
                self.remove_pending_fetch(item_id, request_id);
                Ok(None)
            }
        }
    }

    #[allow(clippy::too_many_arguments)]
    pub(crate) async fn native_request_folder_contents(
        &self,
        folder_id: UUID,
        owner_id: UUID,
        fetch_folders: bool,
        fetch_items: bool,
        order: InventorySortOrder,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<Vec<InventoryBase>, Error> {
        let cancellation_token = cancellation_token.unwrap_or_default();
        cancellation_token.throw_if_cancellation_requested()?;
        let cap_name = if owner_id == self.ids()?.0 {
            "FetchInventoryDescendents2"
        } else {
            "FetchLibDescendents2"
        };
        let capability = if let Ok(simulator) = self.current_simulator(None) {
            self.wait_for_capability(&simulator, cap_name, &cancellation_token)
                .await?
        } else {
            None
        };
        let Some(capability) = capability else {
            self.inner
                .events
                .folder_updated
                .emit(FolderUpdatedEventArgs {
                    folder_id,
                    success: false,
                });
            return Ok(Vec::new());
        };
        let mut folder = InventoryFolder::new(folder_id)?;
        folder.base.set_owner_id(owner_id);
        self.native_request_folder_contents_batch(
            vec![folder],
            capability,
            fetch_folders,
            fetch_items,
            order,
            Some(cancellation_token),
        )
        .await
    }

    pub(crate) async fn native_request_folder_contents_batch(
        &self,
        batch: Vec<InventoryFolder>,
        capability: Uri,
        fetch_folders: bool,
        fetch_items: bool,
        order: InventorySortOrder,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<Vec<InventoryBase>, Error> {
        let cancellation_token = cancellation_token.unwrap_or_default();
        cancellation_token.throw_if_cancellation_requested()?;
        let requested = batch
            .iter()
            .map(|folder| {
                OSD::Map(HashMap::from([
                    ("folder_id".to_owned(), OSD::UUID(folder.base.uuid())),
                    ("owner_id".to_owned(), OSD::UUID(folder.base.owner_id())),
                    ("fetch_folders".to_owned(), OSD::Boolean(fetch_folders)),
                    ("fetch_items".to_owned(), OSD::Boolean(fetch_items)),
                    ("sort_order".to_owned(), OSD::Integer(order.0)),
                ]))
            })
            .collect();
        let body = OSDParser::serialize_llsd_xml_bytes(OSD::Map(HashMap::from([(
            "folders".to_owned(),
            OSD::Array(requested),
        )])))?;
        let response = self
            .client()?
            .native_http_caps_client()
            .post_with_uri_string_bytes_cancellation_token_i_progress(
                capability,
                "application/llsd+xml".to_owned(),
                body,
                cancellation_token,
                None,
            )
            .await;
        let (response, response_body) = match response {
            Ok(response) if response.0.is_success_status_code() => response,
            Ok(_) | Err(_) => {
                for folder in batch {
                    self.inner
                        .events
                        .folder_updated
                        .emit(FolderUpdatedEventArgs {
                            folder_id: folder.base.uuid(),
                            success: false,
                        });
                }
                return Ok(Vec::new());
            }
        };
        let _ = response;
        let OSD::Map(root) = OSDParser::deserialize_with_bytes(response_body)? else {
            return Err(Error::Argument);
        };
        let Some(OSD::Array(folders)) = root.get("folders") else {
            return Ok(Vec::new());
        };
        let store = self.store_clone();
        let mut result = Vec::new();
        for value in folders {
            let OSD::Map(map) = value else {
                continue;
            };
            let folder_id = map.get("folder_id").ok_or(Error::Argument)?.as_uuid()?;
            let owner_id = map.get("owner_id").map_or(Ok(UUID::zero()), OSD::as_uuid)?;
            let mut parent = store
                .as_ref()
                .and_then(|store| {
                    store
                        .get_value_or_default_with_uuid_a7c63fbe::<InventoryFolder>(folder_id)
                        .ok()
                        .flatten()
                })
                .unwrap_or(InventoryFolder::new(folder_id)?);
            parent.base.set_owner_id(owner_id);
            if let Some(value) = map.get("version") {
                parent.set_version(value.as_integer()?);
            }
            if let Some(value) = map.get("descendents") {
                parent.set_descendent_count(value.as_integer()?);
            }
            if let Some(store) = &store {
                store.update_node_for(&parent)?;
            }
            if let Some(OSD::Array(categories)) = map.get("categories") {
                for value in categories {
                    let folder = InventoryFolder::from_osd(value.clone())?;
                    if let Some(store) = &store {
                        store.update_node_for(&folder)?;
                    }
                    result.push(folder.base.clone());
                }
            }
            if let Some(OSD::Array(items)) = map.get("items") {
                for value in items {
                    let item = InventoryItem::from_osd(value.clone())?;
                    self.cache_server_item(&item)?;
                    result.push(item.base.clone());
                }
            }
            self.inner
                .events
                .folder_updated
                .emit(FolderUpdatedEventArgs {
                    folder_id,
                    success: true,
                });
        }
        Ok(result)
    }

    pub(crate) async fn native_request_folder_update(
        &self,
        folder_id: UUID,
        owner_id: UUID,
        fetch_folders: bool,
        fetch_items: bool,
        order: InventorySortOrder,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<InventoryManagerFolderUpdateResult, Error> {
        let cancellation_token = cancellation_token.unwrap_or_default();
        let cap_name = if owner_id == self.ids()?.0 {
            "FetchInventoryDescendents2"
        } else {
            "FetchLibDescendents2"
        };
        let has_capability = if let Ok(simulator) = self.current_simulator(None) {
            self.wait_for_capability(&simulator, cap_name, &cancellation_token)
                .await?
                .is_some()
        } else {
            false
        };
        let contents = self
            .native_request_folder_contents(
                folder_id,
                owner_id,
                fetch_folders,
                fetch_items,
                order,
                Some(cancellation_token),
            )
            .await?;
        Ok(InventoryManagerFolderUpdateResult {
            contents: Some(contents),
            folder_id,
            success: has_capability,
        })
    }

    #[allow(clippy::too_many_arguments)]
    pub(crate) async fn native_folder_contents(
        &self,
        folder_id: UUID,
        owner_id: UUID,
        fetch_folders: bool,
        fetch_items: bool,
        order: InventorySortOrder,
        cancellation_token: Option<CancellationToken>,
        follow_links: bool,
    ) -> Result<Vec<Box<dyn InventoryObjectClass>>, Error> {
        let cancellation_token = cancellation_token.unwrap_or_default();
        let remote = self
            .native_request_folder_contents(
                folder_id,
                owner_id,
                fetch_folders,
                fetch_items,
                order,
                Some(cancellation_token.clone()),
            )
            .await;
        if matches!(remote, Err(Error::Cancelled)) {
            return remote.map(|_| Vec::new());
        }
        let store = self.store_clone();
        let mut contents = store
            .as_ref()
            .and_then(|store| store.get_contents_sorted_with_uuid(folder_id, order).ok())
            .unwrap_or_default();
        if contents.is_empty()
            && let Ok(values) = remote
        {
            contents = values
                .into_iter()
                .map(|value| Box::new(value) as Box<dyn InventoryObjectClass>)
                .collect();
        }
        if follow_links {
            for entry in &mut contents {
                let Some(item) = store
                    .as_ref()
                    .and_then(|store| store.native_item_value(entry.inventory_base().uuid()))
                else {
                    continue;
                };
                if !item.is_link()? {
                    continue;
                }
                if let Some(real) = store
                    .as_ref()
                    .and_then(|store| store.native_item_value(item.asset_uuid()))
                {
                    *entry = typed_inventory_item(real);
                } else if let Some(real) = self
                    .native_fetch_item(
                        item.asset_uuid(),
                        owner_id,
                        Some(cancellation_token.clone()),
                        false,
                    )
                    .await?
                {
                    *entry = typed_inventory_item(real);
                }
            }
        }
        Ok(contents)
    }

    pub(crate) fn native_find_folder_for_type(&self, folder_type: FolderType) -> UUID {
        let Some(store) = self.store_clone() else {
            return UUID::zero();
        };
        store.find_folder_for_type(folder_type).unwrap_or_else(|| {
            store
                .root_folder()
                .map_or_else(UUID::zero, |folder| folder.base.uuid())
        })
    }

    pub(crate) fn native_find_folder_for_asset_type(&self, asset_type: AssetType) -> UUID {
        let Some(folder_type) = folder_type_from_i8_checked(asset_type as i8) else {
            return self
                .store_clone()
                .and_then(|store| store.root_folder())
                .map_or_else(UUID::zero, |folder| folder.base.uuid());
        };
        self.native_find_folder_for_type(folder_type)
    }

    pub(crate) fn native_local_find(
        &self,
        base_folder: UUID,
        path: Vec<String>,
        level: i32,
        first_only: bool,
    ) -> Result<Vec<InventoryBase>, Error> {
        if level < 0 {
            return Ok(Vec::new());
        }
        let Some(store) = self.store_clone() else {
            return Ok(Vec::new());
        };
        let mut visited = HashSet::new();
        local_find_in_store(
            &store,
            base_folder,
            &path,
            usize::try_from(level).map_err(|_| Error::Argument)?,
            first_only,
            &mut visited,
            0,
        )
    }

    pub(crate) async fn native_find_object_by_path(
        &self,
        base_folder: UUID,
        owner_id: UUID,
        path: String,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<UUID, Error> {
        if path.is_empty() {
            return Err(Error::Argument);
        }
        let cancellation_token = cancellation_token.unwrap_or_default();
        let segments: Vec<_> = path.split('/').map(ToOwned::to_owned).collect();
        let mut folder = base_folder;
        for (index, segment) in segments.iter().enumerate() {
            cancellation_token.throw_if_cancellation_requested()?;
            let contents = self
                .native_folder_contents(
                    folder,
                    owner_id,
                    true,
                    true,
                    InventorySortOrder::BY_NAME,
                    Some(cancellation_token.clone()),
                    false,
                )
                .await?;
            let Some(found) = contents
                .into_iter()
                .find(|entry| entry.inventory_base().name() == *segment)
            else {
                self.inner
                    .events
                    .find_object_by_path_reply
                    .emit(FindObjectByPathReplyEventArgs {
                        path,
                        inventory_object_id: UUID::zero(),
                    });
                return Ok(UUID::zero());
            };
            folder = found.inventory_base().uuid();
            if index + 1 == segments.len() {
                self.inner
                    .events
                    .find_object_by_path_reply
                    .emit(FindObjectByPathReplyEventArgs {
                        path,
                        inventory_object_id: folder,
                    });
                return Ok(folder);
            }
        }
        self.inner
            .events
            .find_object_by_path_reply
            .emit(FindObjectByPathReplyEventArgs {
                path,
                inventory_object_id: UUID::zero(),
            });
        Ok(UUID::zero())
    }

    pub(crate) fn native_update_folder_properties(
        &self,
        folder_id: UUID,
        parent_id: UUID,
        name: String,
        folder_type: FolderType,
    ) -> Result<(), Error> {
        let simulator = self.current_simulator(None)?;
        let (agent_id, session_id) = self.ids()?;
        let mut packet = UpdateInventoryFolderPacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent_id;
        packet.agent_data.session_id = session_id;
        let mut block = UpdateInventoryFolderPacketFolderDataBlock::new_with_constructor()?;
        block.folder_id = folder_id;
        block.parent_id = parent_id;
        block.name = Utils::string_to_bytes(name.clone())?;
        block.type_ = folder_type as i8;
        packet.folder_data = vec![block];
        self.send_generated(&simulator, PacketType::UpdateInventoryFolder, &packet)?;
        if let Some(store) = self.store_clone()
            && let Some(mut folder) =
                store.get_value_or_default_with_uuid_a7c63fbe::<InventoryFolder>(folder_id)?
        {
            folder.base.set_parent_uuid(parent_id);
            folder.base.set_name(name);
            folder.set_preferred_type(folder_type);
            store.update_node_for(&folder)?;
        }
        Ok(())
    }

    pub(crate) fn native_move_folders(
        &self,
        moves: HashMap<UUID, UUID>,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<(), Error> {
        cancellation_token
            .unwrap_or_default()
            .throw_if_cancellation_requested()?;
        self.apply_local_folder_moves(&moves)?;
        self.send_move_folders_packet(&moves)
    }

    fn apply_local_folder_moves(&self, moves: &HashMap<UUID, UUID>) -> Result<(), Error> {
        if let Some(store) = self.store_clone() {
            for (folder_id, parent_id) in moves {
                if let Some(mut folder) =
                    store.get_value_or_default_with_uuid_a7c63fbe::<InventoryFolder>(*folder_id)?
                {
                    folder.base.set_parent_uuid(*parent_id);
                    store.update_node_for(&folder)?;
                }
            }
        }
        Ok(())
    }

    fn send_move_folders_packet(&self, moves: &HashMap<UUID, UUID>) -> Result<(), Error> {
        let simulator = self.current_simulator(None)?;
        let (agent_id, session_id) = self.ids()?;
        let mut packet = MoveInventoryFolderPacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent_id;
        packet.agent_data.session_id = session_id;
        packet.agent_data.stamp = false;
        packet.inventory_data = moves
            .iter()
            .map(|(folder_id, parent_id)| {
                let mut block =
                    MoveInventoryFolderPacketInventoryDataBlock::new_with_constructor()?;
                block.folder_id = *folder_id;
                block.parent_id = *parent_id;
                Ok(block)
            })
            .collect::<Result<Vec<_>, Error>>()?;
        self.send_generated(&simulator, PacketType::MoveInventoryFolder, &packet)
    }

    pub(crate) fn native_move_items(
        &self,
        moves: HashMap<UUID, (UUID, String)>,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<(), Error> {
        cancellation_token
            .unwrap_or_default()
            .throw_if_cancellation_requested()?;
        self.apply_local_item_moves(&moves)?;
        self.send_move_items_packet(&moves)
    }

    fn apply_local_item_moves(&self, moves: &HashMap<UUID, (UUID, String)>) -> Result<(), Error> {
        if let Some(store) = self.store_clone() {
            for (item_id, (folder_id, new_name)) in moves {
                if let Some(mut item) = store.native_item_value(*item_id) {
                    item.base.set_parent_uuid(*folder_id);
                    if !new_name.is_empty() {
                        item.base.set_name(new_name.clone());
                    }
                    store.update_node_for(&item)?;
                }
            }
        }
        Ok(())
    }

    fn send_move_items_packet(&self, moves: &HashMap<UUID, (UUID, String)>) -> Result<(), Error> {
        let simulator = self.current_simulator(None)?;
        let (agent_id, session_id) = self.ids()?;
        let mut packet = MoveInventoryItemPacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent_id;
        packet.agent_data.session_id = session_id;
        packet.agent_data.stamp = false;
        packet.inventory_data = moves
            .iter()
            .map(|(item_id, (folder_id, name))| {
                let mut block = MoveInventoryItemPacketInventoryDataBlock::new_with_constructor()?;
                block.item_id = *item_id;
                block.folder_id = *folder_id;
                block.new_name = Utils::string_to_bytes(name.clone())?;
                Ok(block)
            })
            .collect::<Result<Vec<_>, Error>>()?;
        self.send_generated(&simulator, PacketType::MoveInventoryItem, &packet)
    }

    pub(crate) async fn native_move_folders_async(
        &self,
        moves: HashMap<UUID, UUID>,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<(), Error> {
        let cancellation_token = cancellation_token.unwrap_or_default();
        cancellation_token.throw_if_cancellation_requested()?;
        self.apply_local_folder_moves(&moves)?;
        if let Some(capability) = self.inventory_api_capability(&cancellation_token).await? {
            for (folder_id, parent_id) in moves {
                cancellation_token.throw_if_cancellation_requested()?;
                let _ = self
                    .ais_patch_parent(
                        &capability,
                        "category",
                        folder_id,
                        parent_id,
                        cancellation_token.clone(),
                    )
                    .await;
            }
            return Ok(());
        }
        self.send_move_folders_packet(&moves)
    }

    pub(crate) async fn native_move_items_async(
        &self,
        moves: HashMap<UUID, (UUID, String)>,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<(), Error> {
        let cancellation_token = cancellation_token.unwrap_or_default();
        cancellation_token.throw_if_cancellation_requested()?;
        self.apply_local_item_moves(&moves)?;
        let can_use_ais = moves.values().all(|(_, name)| name.is_empty());
        if can_use_ais
            && let Some(capability) = self.inventory_api_capability(&cancellation_token).await?
        {
            for (item_id, (parent_id, _)) in moves {
                cancellation_token.throw_if_cancellation_requested()?;
                let _ = self
                    .ais_patch_parent(
                        &capability,
                        "item",
                        item_id,
                        parent_id,
                        cancellation_token.clone(),
                    )
                    .await;
            }
            return Ok(());
        }
        self.send_move_items_packet(&moves)
    }

    async fn inventory_api_capability(
        &self,
        cancellation_token: &CancellationToken,
    ) -> Result<Option<Uri>, Error> {
        let Ok(simulator) = self.current_simulator(None) else {
            return Ok(None);
        };
        self.wait_for_capability(&simulator, "InventoryAPIv3", cancellation_token)
            .await
    }

    async fn ais_patch_parent(
        &self,
        capability: &Uri,
        kind: &str,
        object_id: UUID,
        parent_id: UUID,
        cancellation_token: CancellationToken,
    ) -> Result<bool, Error> {
        let uri = Uri(format!(
            "{}/{kind}/{object_id}",
            capability.0.trim_end_matches('/')
        ));
        let body = OSDParser::serialize_llsd_xml_bytes(OSD::Map(HashMap::from([(
            "parent_id".to_owned(),
            OSD::UUID(parent_id),
        )])))?;
        let response = self
            .client()?
            .native_http_caps_client()
            .patch_with_uri_string_bytes_cancellation_token_i_progress(
                uri,
                "application/llsd+xml".to_owned(),
                body,
                cancellation_token,
                None,
            )
            .await?;
        Ok(response.0.is_success_status_code())
    }

    pub(crate) fn native_move_object(
        &self,
        item: InventoryBase,
        new_parent: InventoryFolder,
    ) -> Result<(), Error> {
        if self
            .store_clone()
            .and_then(|store| {
                store
                    .get_value_or_default_with_uuid_a7c63fbe::<InventoryFolder>(item.uuid())
                    .ok()
                    .flatten()
            })
            .is_some()
        {
            self.native_move_folders(HashMap::from([(item.uuid(), new_parent.base.uuid())]), None)
        } else {
            self.native_move_items(
                HashMap::from([(item.uuid(), (new_parent.base.uuid(), String::new()))]),
                None,
            )
        }
    }

    pub(crate) fn native_request_update_items(
        &self,
        items: Vec<InventoryItem>,
        transaction_id: UUID,
    ) -> Result<(), Error> {
        let simulator = self.current_simulator(None)?;
        let (agent_id, session_id) = self.ids()?;
        let mut packet = UpdateInventoryItemPacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent_id;
        packet.agent_data.session_id = session_id;
        packet.agent_data.transaction_id = transaction_id;
        packet.inventory_data = items
            .iter()
            .map(|item| {
                let mut block =
                    UpdateInventoryItemPacketInventoryDataBlock::new_with_constructor()?;
                fill_inventory_wire_block!(
                    block,
                    item,
                    item.transaction_id(),
                    item.asset_type() as i8
                );
                block.callback_id = 0;
                block.crc = Self::native_item_crc(item)?;
                Ok(block)
            })
            .collect::<Result<Vec<_>, Error>>()?;
        self.send_generated(&simulator, PacketType::UpdateInventoryItem, &packet)
    }

    pub(crate) async fn native_remove_descendants(
        &self,
        folder_id: UUID,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<(), Error> {
        let cancellation_token = cancellation_token.unwrap_or_default();
        cancellation_token.throw_if_cancellation_requested()?;
        if let Some(capability) = self.inventory_api_capability(&cancellation_token).await? {
            if self
                .ais_delete_object(
                    &capability,
                    &format!("category/{folder_id}/children"),
                    cancellation_token,
                )
                .await
                .unwrap_or(false)
            {
                self.remove_local_descendants(folder_id, true)?;
            }
            return Ok(());
        }
        let simulator = self.current_simulator(None)?;
        let (agent_id, session_id) = self.ids()?;
        let mut packet = PurgeInventoryDescendentsPacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent_id;
        packet.agent_data.session_id = session_id;
        packet.inventory_data.folder_id = folder_id;
        self.send_generated(&simulator, PacketType::PurgeInventoryDescendents, &packet)?;
        self.remove_local_descendants(folder_id, true)
    }

    pub(crate) async fn native_remove_inventory_objects(
        &self,
        items: Vec<UUID>,
        folders: Vec<UUID>,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<(), Error> {
        let cancellation_token = cancellation_token.unwrap_or_default();
        cancellation_token.throw_if_cancellation_requested()?;
        if let Some(capability) = self.inventory_api_capability(&cancellation_token).await? {
            for item in items {
                cancellation_token.throw_if_cancellation_requested()?;
                if self
                    .ais_delete_object(
                        &capability,
                        &format!("item/{item}"),
                        cancellation_token.clone(),
                    )
                    .await
                    .unwrap_or(false)
                {
                    self.remove_local_descendants(item, true)?;
                }
            }
            for folder in folders {
                cancellation_token.throw_if_cancellation_requested()?;
                if self
                    .ais_delete_object(
                        &capability,
                        &format!("category/{folder}"),
                        cancellation_token.clone(),
                    )
                    .await
                    .unwrap_or(false)
                {
                    self.remove_local_descendants(folder, true)?;
                }
            }
            return Ok(());
        }
        let simulator = self.current_simulator(None)?;
        let (agent_id, session_id) = self.ids()?;
        let mut packet = RemoveInventoryObjectsPacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent_id;
        packet.agent_data.session_id = session_id;
        packet.item_data = items
            .iter()
            .map(|item_id| {
                let mut block = RemoveInventoryObjectsPacketItemDataBlock::new_with_constructor()?;
                block.item_id = *item_id;
                Ok(block)
            })
            .collect::<Result<Vec<_>, Error>>()?;
        packet.folder_data = folders
            .iter()
            .map(|folder_id| {
                let mut block =
                    RemoveInventoryObjectsPacketFolderDataBlock::new_with_constructor()?;
                block.folder_id = *folder_id;
                Ok(block)
            })
            .collect::<Result<Vec<_>, Error>>()?;
        self.send_generated(&simulator, PacketType::RemoveInventoryObjects, &packet)?;
        for item in items {
            self.remove_local_descendants(item, true)?;
        }
        for folder in folders {
            self.remove_local_descendants(folder, true)?;
        }
        Ok(())
    }

    async fn ais_delete_object(
        &self,
        capability: &Uri,
        path: &str,
        cancellation_token: CancellationToken,
    ) -> Result<bool, Error> {
        let uri = Uri(format!("{}/{path}", capability.0.trim_end_matches('/')));
        let response = self
            .client()?
            .native_http_caps_client()
            .delete_with_uri_string_bytes_cancellation_token_i_progress(
                uri,
                "application/llsd+xml".to_owned(),
                Vec::new(),
                cancellation_token,
                None,
            )
            .await?;
        Ok(response.0.is_success_status_code() || response.0.status_code == 410)
    }

    fn remove_local_descendants(&self, root: UUID, include_root: bool) -> Result<(), Error> {
        let Some(store) = self.store_clone() else {
            return Ok(());
        };
        if include_root {
            if let Some(value) = store.get_value_or_default_with_uuid(root)? {
                store.remove_node_for(&value)?;
            }
            return Ok(());
        }
        let contents = store.get_contents_with_uuid(root).unwrap_or_default();
        for value in contents {
            store.remove_node_for(value.as_ref())?;
        }
        Ok(())
    }

    pub(crate) fn native_empty_folder(&self, folder_id: UUID) -> Result<(), Error> {
        let Some(store) = self.store_clone() else {
            return Ok(());
        };
        let contents = store.get_contents_with_uuid(folder_id).unwrap_or_default();
        let mut items = Vec::new();
        let mut folders = Vec::new();
        for value in contents {
            if value.as_any().is::<InventoryFolder>() {
                folders.push(value.inventory_base().uuid());
            } else {
                items.push(value.inventory_base().uuid());
            }
        }
        let simulator = self.current_simulator(None)?;
        let (agent_id, session_id) = self.ids()?;
        let mut packet = RemoveInventoryObjectsPacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent_id;
        packet.agent_data.session_id = session_id;
        packet.item_data = items
            .iter()
            .map(|id| {
                let mut block = RemoveInventoryObjectsPacketItemDataBlock::new_with_constructor()?;
                block.item_id = *id;
                Ok(block)
            })
            .collect::<Result<Vec<_>, Error>>()?;
        packet.folder_data = folders
            .iter()
            .map(|id| {
                let mut block =
                    RemoveInventoryObjectsPacketFolderDataBlock::new_with_constructor()?;
                block.folder_id = *id;
                Ok(block)
            })
            .collect::<Result<Vec<_>, Error>>()?;
        self.send_generated(&simulator, PacketType::RemoveInventoryObjects, &packet)
    }

    pub(crate) async fn native_empty_system_folder(
        &self,
        folder_type: FolderType,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<(), Error> {
        let cancellation_token = cancellation_token.unwrap_or_default();
        cancellation_token.throw_if_cancellation_requested()?;
        let folder_id = self.native_find_folder_for_type(folder_type);
        if folder_id != UUID::zero() {
            self.native_remove_descendants(folder_id, Some(cancellation_token))
                .await?;
        }
        Ok(())
    }

    fn copy_from_notecard_packet(
        &self,
        object_id: UUID,
        notecard_id: UUID,
        folder_id: UUID,
        item_id: UUID,
    ) -> Result<CopyInventoryFromNotecardPacket, Error> {
        let (agent_id, session_id) = self.ids()?;
        let mut packet = CopyInventoryFromNotecardPacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent_id;
        packet.agent_data.session_id = session_id;
        packet.notecard_data.object_id = object_id;
        packet.notecard_data.notecard_item_id = notecard_id;
        let mut block = CopyInventoryFromNotecardPacketInventoryDataBlock::new_with_constructor()?;
        block.folder_id = folder_id;
        block.item_id = item_id;
        packet.inventory_data = vec![block];
        Ok(packet)
    }

    #[allow(clippy::too_many_arguments)]
    pub(crate) async fn native_copy_item_from_notecard(
        &self,
        object_id: UUID,
        notecard_id: UUID,
        folder_id: UUID,
        item_id: UUID,
        cancellation_token: Option<CancellationToken>,
        progress: Option<
            Box<dyn libremetaverse_types::compat::IProgress<crate::HttpCapsClientProgressReport>>,
        >,
    ) -> Result<Option<InventoryBase>, Error> {
        const NOTECARD_CALLBACK_ID: u32 = 0;

        let cancellation_token = cancellation_token.unwrap_or_default();
        cancellation_token.throw_if_cancellation_requested()?;
        self.ensure_pending_capacity()?;
        let (sender, receiver) = oneshot::channel();
        {
            let mut copied = mutex(&self.inner.pending_copied);
            // This protocol fixes the callback ID at zero. Reject a concurrent
            // request rather than overwriting the only correlatable waiter.
            if copied.contains_key(&NOTECARD_CALLBACK_ID) {
                return Err(Error::InvalidOperation);
            }
            copied.insert(
                NOTECARD_CALLBACK_ID,
                PendingCopy {
                    deadline: Instant::now() + CALLBACK_TIMEOUT,
                    expected: 1,
                    copied: Vec::with_capacity(1),
                    sender,
                },
            );
        }

        let send_result = async {
            let simulator = self.current_simulator(None)?;
            if let Some(capability) = self
                .wait_for_capability(&simulator, "CopyInventoryFromNotecard", &cancellation_token)
                .await?
            {
                let mut message = CopyInventoryFromNotecardMessage::new()?;
                message.callback_id = 0;
                message.folder_id = folder_id;
                message.item_id = item_id;
                message.notecard_id = notecard_id;
                message.object_id = object_id;
                let body =
                    OSDParser::serialize_llsd_xml_bytes(OSD::Map(message.serialize()?.snapshot()))?;
                match self
                    .client()?
                    .native_http_caps_client()
                    .post_with_uri_string_bytes_cancellation_token_i_progress(
                        capability,
                        "application/llsd+xml".to_owned(),
                        body,
                        cancellation_token.clone(),
                        progress,
                    )
                    .await
                {
                    Ok((response, _)) if response.is_success_status_code() => return Ok(()),
                    Err(Error::Cancelled) => return Err(Error::Cancelled),
                    Ok(_) | Err(_) => {}
                }
            }
            cancellation_token.throw_if_cancellation_requested()?;
            let packet =
                self.copy_from_notecard_packet(object_id, notecard_id, folder_id, item_id)?;
            self.send_generated(&simulator, PacketType::CopyInventoryFromNotecard, &packet)
        }
        .await;
        if let Err(error) = send_result {
            mutex(&self.inner.pending_copied).remove(&NOTECARD_CALLBACK_ID);
            return Err(error);
        }

        let cancelled = cancellation_token.cancelled();
        futures_util::pin_mut!(receiver, cancelled);
        match select(receiver, cancelled).await {
            Either::Left((Ok(result), _)) => Ok(result
                .copied_items
                .and_then(|mut items| (!items.is_empty()).then(|| items.remove(0)))),
            Either::Left((Err(_), _)) => Ok(None),
            Either::Right(_) => {
                mutex(&self.inner.pending_copied).remove(&NOTECARD_CALLBACK_ID);
                Err(Error::Cancelled)
            }
        }
    }

    pub(crate) async fn native_copy_items(
        &self,
        items: Vec<UUID>,
        target_folders: Vec<UUID>,
        new_names: Vec<String>,
        old_owner_id: UUID,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<InventoryManagerCopyItemsResult, Error> {
        if items.len() != target_folders.len()
            || (!new_names.is_empty() && items.len() != new_names.len())
        {
            return Err(Error::Argument);
        }
        let cancellation_token = cancellation_token.unwrap_or_default();
        cancellation_token.throw_if_cancellation_requested()?;
        if items.is_empty() {
            return Ok(InventoryManagerCopyItemsResult {
                copied_items: Some(Vec::new()),
                error: None,
                success: true,
            });
        }

        if let Ok(simulator) = self.current_simulator(None)
            && let Some(capability) = self
                .wait_for_capability(&simulator, "InventoryAPIv3", &cancellation_token)
                .await?
        {
            let operations = items
                .iter()
                .zip(&target_folders)
                .enumerate()
                .map(|(index, (item_id, folder_id))| {
                    let mut operation = HashMap::from([
                        ("item_id".to_owned(), OSD::UUID(*item_id)),
                        ("folder_id".to_owned(), OSD::UUID(*folder_id)),
                    ]);
                    if let Some(name) = new_names.get(index)
                        && !name.is_empty()
                    {
                        operation.insert("new_name".to_owned(), OSD::String(name.clone()));
                    }
                    OSD::Map(operation)
                })
                .collect();
            let payload = OSD::Map(HashMap::from([
                ("items".to_owned(), OSD::Array(operations)),
                ("agent_id".to_owned(), OSD::UUID(self.ids()?.0)),
            ]));
            let body = OSDParser::serialize_llsd_xml_bytes(payload)?;
            let response = self
                .client()?
                .native_http_caps_client()
                .post_with_uri_string_bytes_cancellation_token_i_progress(
                    capability,
                    "application/llsd+xml".to_owned(),
                    body,
                    cancellation_token.clone(),
                    None,
                )
                .await;
            if response.is_ok_and(|response| response.0.is_success_status_code()) {
                return Ok(InventoryManagerCopyItemsResult {
                    copied_items: Some(Vec::new()),
                    error: None,
                    success: true,
                });
            }
        }

        self.ensure_pending_capacity()?;
        let callback_id = self.next_callback_id();
        let (sender, receiver) = oneshot::channel();
        mutex(&self.inner.pending_copied).insert(
            callback_id,
            PendingCopy {
                deadline: Instant::now() + CALLBACK_TIMEOUT,
                expected: items.len(),
                copied: Vec::with_capacity(items.len()),
                sender,
            },
        );
        let send_result = (|| {
            let simulator = self.current_simulator(None)?;
            let (agent_id, session_id) = self.ids()?;
            let mut packet = CopyInventoryItemPacket::new_with_constructor()?;
            packet.agent_data.agent_id = agent_id;
            packet.agent_data.session_id = session_id;
            packet.inventory_data = items
                .iter()
                .zip(&target_folders)
                .enumerate()
                .map(|(index, (item_id, folder_id))| {
                    let mut block =
                        CopyInventoryItemPacketInventoryDataBlock::new_with_constructor()?;
                    block.callback_id = callback_id;
                    block.old_item_id = *item_id;
                    block.old_agent_id = old_owner_id;
                    block.new_folder_id = *folder_id;
                    block.new_name =
                        Utils::string_to_bytes(new_names.get(index).cloned().unwrap_or_default())?;
                    Ok(block)
                })
                .collect::<Result<Vec<_>, Error>>()?;
            self.send_generated(&simulator, PacketType::CopyInventoryItem, &packet)
        })();
        if let Err(error) = send_result {
            mutex(&self.inner.pending_copied).remove(&callback_id);
            return Ok(InventoryManagerCopyItemsResult {
                copied_items: None,
                error: Some(ExternalError(error.to_string())),
                success: false,
            });
        }
        let cancelled = cancellation_token.cancelled();
        futures_util::pin_mut!(receiver, cancelled);
        match select(receiver, cancelled).await {
            Either::Left((Ok(result), _)) => Ok(result),
            Either::Left((Err(_), _)) => Ok(InventoryManagerCopyItemsResult::default()),
            Either::Right(_) => {
                mutex(&self.inner.pending_copied).remove(&callback_id);
                Err(Error::Cancelled)
            }
        }
    }

    pub(crate) async fn native_copy_item(
        &self,
        item_id: UUID,
        new_parent: UUID,
        new_name: String,
        old_owner_id: UUID,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<Option<InventoryBase>, Error> {
        Ok(self
            .native_copy_items(
                vec![item_id],
                vec![new_parent],
                vec![new_name],
                old_owner_id,
                cancellation_token,
            )
            .await?
            .copied_items
            .and_then(|mut items| (!items.is_empty()).then(|| items.remove(0))))
    }

    pub(crate) async fn native_copy_item_default_owner(
        &self,
        item_id: UUID,
        new_parent: UUID,
        new_name: String,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<Option<InventoryBase>, Error> {
        self.native_copy_item(
            item_id,
            new_parent,
            new_name,
            self.ids()?.0,
            cancellation_token,
        )
        .await
    }

    pub(crate) fn native_dispose(&self) -> Result<(), Error> {
        if self.inner.disposed.swap(true, Ordering::AcqRel) {
            return Ok(());
        }
        if let Some(worker) = mutex(&self.inner.cleanup_worker).take() {
            let (stopped, wake) = &*worker.stop;
            *mutex(stopped) = true;
            wake.notify_all();
            if worker.handle.thread().id() != std::thread::current().id() {
                let _ = worker.handle.join();
            }
        }
        mutex(&self.inner.raw_packet_subscription).take();
        mutex(&self.inner.caps_subscription).take();
        mutex(&self.inner.im_subscription).take();
        if let Some(callback) = mutex(&self.inner.login_callback).take()
            && let Ok(client) = self.inner.client.upgrade().ok_or(Error::InvalidOperation)
            && let Ok(network) = client.native_network()
        {
            let _ = network.native_unregister_login_response_callback(&callback);
        }
        let created = mutex(&self.inner.pending_created)
            .drain()
            .map(|(_, v)| v)
            .collect::<Vec<_>>();
        let copied = mutex(&self.inner.pending_copied)
            .drain()
            .map(|(_, v)| v)
            .collect::<Vec<_>>();
        let fetches = mutex(&self.inner.pending_fetches)
            .drain()
            .flat_map(|(_, v)| v)
            .collect::<Vec<_>>();
        let task_replies = mutex(&self.inner.pending_task_replies)
            .drain()
            .flat_map(|(_, v)| v)
            .collect::<Vec<_>>();
        let offers = std::mem::take(&mut *mutex(&self.inner.pending_offers));
        let xfers = mutex(&self.inner.task_xfers)
            .drain()
            .map(|(_, v)| v)
            .collect::<Vec<_>>();
        for request in created {
            let _ = request.sender.send(None);
        }
        for request in copied {
            let _ = request
                .sender
                .send(InventoryManagerCopyItemsResult::default());
        }
        for request in fetches {
            let _ = request.sender.send(None);
        }
        for request in task_replies {
            let _ = request.sender.send(String::new());
        }
        drop(offers);
        for request in xfers {
            let _ = request.sender.send(None);
        }
        write(&self.inner.store).take();
        Ok(())
    }

    fn current_simulator(&self, simulator: Option<Simulator>) -> Result<Simulator, Error> {
        simulator
            .or_else(|| {
                self.client()
                    .ok()?
                    .native_network()
                    .ok()?
                    .native_current_sim()
            })
            .ok_or(Error::InvalidOperation)
    }

    fn ids(&self) -> Result<(UUID, UUID), Error> {
        let network = self.client()?.native_network()?;
        Ok((network.native_agent_id(), network.native_session_id()))
    }

    fn send_encoded(
        simulator: &Simulator,
        packet_type: PacketType,
        bytes: Vec<u8>,
    ) -> Result<(), Error> {
        simulator.native_send_packet_data(
            bytes.clone(),
            i32::try_from(bytes.len()).map_err(|_| Error::Argument)?,
            packet_type,
            false,
        )
    }

    #[allow(clippy::unused_self)] // Kept as a manager method to centralize all outbound packet sends.
    fn send_generated<P: GeneratedPacket>(
        &self,
        simulator: &Simulator,
        packet_type: PacketType,
        packet: &P,
    ) -> Result<(), Error> {
        for bytes in packet.encode_multiple()? {
            Self::send_encoded(simulator, packet_type, bytes)?;
        }
        Ok(())
    }

    fn next_callback_id(&self) -> u32 {
        loop {
            let value = self
                .inner
                .callback_position
                .fetch_add(1, Ordering::Relaxed)
                .wrapping_add(1);
            if value != 0 {
                return value;
            }
        }
    }

    fn next_request_id(&self) -> u64 {
        loop {
            let value = self
                .inner
                .request_position
                .fetch_add(1, Ordering::Relaxed)
                .wrapping_add(1);
            if value != 0 {
                return value;
            }
        }
    }

    fn ensure_pending_capacity(&self) -> Result<(), Error> {
        let total = mutex(&self.inner.pending_created).len()
            + mutex(&self.inner.pending_copied).len()
            + mutex(&self.inner.pending_fetches)
                .values()
                .map(Vec::len)
                .sum::<usize>()
            + mutex(&self.inner.pending_task_replies)
                .values()
                .map(Vec::len)
                .sum::<usize>()
            + mutex(&self.inner.pending_offers).len()
            + mutex(&self.inner.task_xfers).len();
        if total >= MAX_PENDING_REQUESTS {
            Err(Error::InvalidOperation)
        } else {
            Ok(())
        }
    }

    fn remove_pending_fetch(&self, item_id: UUID, request_id: u64) {
        let mut pending = mutex(&self.inner.pending_fetches);
        if let Some(waiters) = pending.get_mut(&item_id) {
            waiters.retain(|waiter| waiter.request_id != request_id);
            if waiters.is_empty() {
                pending.remove(&item_id);
            }
        }
    }

    fn remove_pending_task_reply(&self, object_id: UUID, request_id: u64) {
        let mut pending = mutex(&self.inner.pending_task_replies);
        if let Some(waiters) = pending.get_mut(&object_id) {
            waiters.retain(|waiter| waiter.request_id != request_id);
            if waiters.is_empty() {
                pending.remove(&object_id);
            }
        }
    }

    pub(crate) fn native_request_task_inventory(
        &self,
        object_local_id: u32,
        simulator: Option<Simulator>,
    ) -> Result<(), Error> {
        let simulator = self.current_simulator(simulator)?;
        let (agent_id, session_id) = self.ids()?;
        let mut packet = RequestTaskInventoryPacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent_id;
        packet.agent_data.session_id = session_id;
        packet.inventory_data.local_id = object_local_id;
        Self::send_encoded(
            &simulator,
            PacketType::RequestTaskInventory,
            packet.to_bytes_with_method()?,
        )
    }

    pub(crate) fn native_update_task_inventory(
        &self,
        object_local_id: u32,
        item: InventoryItem,
        simulator: Option<Simulator>,
        only_modify_metadata: bool,
    ) -> Result<UUID, Error> {
        let simulator = self.current_simulator(simulator)?;
        let transaction_id = if only_modify_metadata {
            UUID::zero()
        } else {
            UUID::random()?
        };
        let (agent_id, session_id) = self.ids()?;
        let mut packet = UpdateTaskInventoryPacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent_id;
        packet.agent_data.session_id = session_id;
        packet.update_data.key = 0;
        packet.update_data.local_id = object_local_id;
        fill_inventory_wire_block!(
            packet.inventory_data,
            &item,
            transaction_id,
            item.asset_type() as i8
        );
        packet.inventory_data.crc = Self::native_item_crc(&item)?;
        self.send_generated(&simulator, PacketType::UpdateTaskInventory, &packet)?;
        Ok(transaction_id)
    }

    pub(crate) fn native_move_task_inventory(
        &self,
        object_local_id: u32,
        task_item_id: UUID,
        inventory_folder_id: UUID,
        simulator: Simulator,
    ) -> Result<(), Error> {
        let (agent_id, session_id) = self.ids()?;
        let mut packet = MoveTaskInventoryPacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent_id;
        packet.agent_data.session_id = session_id;
        packet.agent_data.folder_id = inventory_folder_id;
        packet.inventory_data.item_id = task_item_id;
        packet.inventory_data.local_id = object_local_id;
        self.send_generated(&simulator, PacketType::MoveTaskInventory, &packet)
    }

    pub(crate) fn native_remove_task_inventory(
        &self,
        object_local_id: u32,
        task_item_id: UUID,
        simulator: Simulator,
    ) -> Result<(), Error> {
        let (agent_id, session_id) = self.ids()?;
        let mut packet = RemoveTaskInventoryPacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent_id;
        packet.agent_data.session_id = session_id;
        packet.inventory_data.item_id = task_item_id;
        packet.inventory_data.local_id = object_local_id;
        self.send_generated(&simulator, PacketType::RemoveTaskInventory, &packet)
    }

    pub(crate) fn native_copy_script_to_task(
        &self,
        object_local_id: u32,
        item: InventoryItem,
        enable_script: bool,
        simulator: Option<Simulator>,
    ) -> Result<UUID, Error> {
        let simulator = self.current_simulator(simulator)?;
        let transaction_id = UUID::random()?;
        let (agent_id, session_id) = self.ids()?;
        let mut packet = RezScriptPacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent_id;
        packet.agent_data.session_id = session_id;
        packet.update_block.object_local_id = object_local_id;
        packet.update_block.enabled = enable_script;
        fill_inventory_wire_block!(
            packet.inventory_block,
            &item,
            transaction_id,
            item.asset_type() as i8
        );
        packet.inventory_block.crc = Self::native_item_crc(&item)?;
        self.send_generated(&simulator, PacketType::RezScript, &packet)?;
        Ok(transaction_id)
    }

    pub(crate) fn native_request_get_script_running(
        &self,
        object_id: UUID,
        script_id: UUID,
    ) -> Result<(), Error> {
        let simulator = self.current_simulator(None)?;
        let mut packet = GetScriptRunningPacket::new_with_constructor()?;
        packet.script.object_id = object_id;
        packet.script.item_id = script_id;
        self.send_generated(&simulator, PacketType::GetScriptRunning, &packet)
    }

    pub(crate) fn native_request_set_script_running(
        &self,
        object_id: UUID,
        script_id: UUID,
        running: bool,
    ) -> Result<(), Error> {
        let simulator = self.current_simulator(None)?;
        let (agent_id, session_id) = self.ids()?;
        let mut packet = SetScriptRunningPacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent_id;
        packet.agent_data.session_id = session_id;
        packet.script.object_id = object_id;
        packet.script.item_id = script_id;
        packet.script.running = running;
        self.send_generated(&simulator, PacketType::SetScriptRunning, &packet)
    }

    #[allow(clippy::too_many_arguments)]
    pub(crate) fn native_rez_from_inventory(
        &self,
        simulator: Simulator,
        task_id: UUID,
        _rotation: Quaternion,
        position: Vector3,
        item: InventoryItem,
        group_owner: UUID,
        query_id: UUID,
        rez_selected: bool,
    ) -> Result<UUID, Error> {
        let (agent_id, session_id) = self.ids()?;
        let permissions = item.permissions();
        let mut packet = RezObjectPacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent_id;
        packet.agent_data.session_id = session_id;
        packet.agent_data.group_id = group_owner;
        packet.rez_data.from_task_id = task_id;
        packet.rez_data.bypass_raycast = 1;
        packet.rez_data.ray_start = position;
        packet.rez_data.ray_end = position;
        packet.rez_data.ray_target_id = UUID::zero();
        packet.rez_data.ray_end_is_intersection = false;
        packet.rez_data.rez_selected = rez_selected;
        packet.rez_data.remove_item = false;
        packet.rez_data.item_flags = item.flags();
        packet.rez_data.group_mask = permissions.group_mask.0;
        packet.rez_data.everyone_mask = permissions.everyone_mask.0;
        packet.rez_data.next_owner_mask = permissions.next_owner_mask.0;
        fill_inventory_wire_block!(
            packet.inventory_data,
            &item,
            query_id,
            item.inventory_type().0
        );
        self.send_generated(&simulator, PacketType::RezObject, &packet)?;
        self.remove_if_no_copy(item.base.uuid())?;
        Ok(query_id)
    }

    pub(crate) fn native_rez_from_inventory_default(
        &self,
        simulator: Simulator,
        rotation: Quaternion,
        position: Vector3,
        item: InventoryItem,
        group_owner: Option<UUID>,
    ) -> Result<UUID, Error> {
        let group_owner = if let Some(group_owner) = group_owner {
            group_owner
        } else {
            let mut client = self.client()?;
            client.native_self().native_active_group()
        };
        self.native_rez_from_inventory(
            simulator,
            UUID::zero(),
            rotation,
            position,
            item,
            group_owner,
            UUID::random()?,
            true,
        )
    }

    pub(crate) fn native_restore_rez_from_inventory(
        &self,
        simulator: Simulator,
        item: InventoryItem,
        query_id: UUID,
    ) -> Result<UUID, Error> {
        let (agent_id, session_id) = self.ids()?;
        let mut packet = RezRestoreToWorldPacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent_id;
        packet.agent_data.session_id = session_id;
        fill_inventory_wire_block!(
            packet.inventory_data,
            &item,
            query_id,
            item.inventory_type().0
        );
        self.send_generated(&simulator, PacketType::RezRestoreToWorld, &packet)?;
        Ok(query_id)
    }

    pub(crate) fn native_de_rez_to_inventory(
        &self,
        object_local_id: u32,
        destination: DeRezDestination,
        destination_id: UUID,
        transaction_id: UUID,
    ) -> Result<(), Error> {
        let simulator = self.current_simulator(None)?;
        let (agent_id, session_id) = self.ids()?;
        let mut packet = DeRezObjectPacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent_id;
        packet.agent_data.session_id = session_id;
        packet.agent_block.group_id = UUID::zero();
        packet.agent_block.destination = destination as u8;
        packet.agent_block.destination_id = destination_id;
        packet.agent_block.packet_count = 1;
        packet.agent_block.packet_number = 1;
        packet.agent_block.transaction_id = transaction_id;
        let mut block = DeRezObjectPacketObjectDataBlock::new_with_constructor()?;
        block.object_local_id = object_local_id;
        packet.object_data = vec![block];
        self.send_generated(&simulator, PacketType::DeRezObject, &packet)
    }

    pub(crate) fn native_de_rez_to_objects_folder(
        &self,
        object_local_id: u32,
    ) -> Result<(), Error> {
        self.native_de_rez_to_inventory(
            object_local_id,
            DeRezDestination::AgentInventoryTake,
            self.native_find_folder_for_type(FolderType::Object),
            UUID::random()?,
        )
    }

    pub(crate) fn native_give_item(
        &self,
        item_id: UUID,
        item_name: String,
        asset_type: AssetType,
        recipient: UUID,
        do_effect: bool,
    ) -> Result<(), Error> {
        let mut bucket = Vec::with_capacity(17);
        bucket.push(asset_type as u8);
        bucket.extend_from_slice(&item_id.get_bytes()?);
        self.send_inventory_offer(recipient, item_name, bucket, do_effect)?;
        self.remove_if_no_copy(item_id)
    }

    pub(crate) async fn native_give_folder(
        &self,
        folder_id: UUID,
        folder_name: String,
        recipient: UUID,
        do_effect: bool,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<(), Error> {
        let cancellation_token = cancellation_token.unwrap_or_default();
        let (folders, items) = self
            .native_collect_inventory_recursive(
                folder_id,
                self.ids()?.0,
                Some(cancellation_token.clone()),
            )
            .await?;
        if folders.len() + items.len() > Self::MAX_GIVE_ITEMS as usize || items.is_empty() {
            return Ok(());
        }
        let mut bucket = Vec::with_capacity(17 * (folders.len() + items.len() + 1));
        bucket.push(AssetType::Folder as u8);
        bucket.extend_from_slice(&folder_id.get_bytes()?);
        for folder in &folders {
            bucket.push(AssetType::Folder as u8);
            bucket.extend_from_slice(&folder.base.uuid().get_bytes()?);
        }
        for item in &items {
            bucket.push(item.asset_type() as u8);
            bucket.extend_from_slice(&item.base.uuid().get_bytes()?);
        }
        self.send_inventory_offer(recipient, folder_name, bucket, do_effect)?;
        for item in items {
            self.remove_if_no_copy(item.base.uuid())?;
        }
        Ok(())
    }

    pub(crate) async fn native_wait_for_next_inventory_offer(
        &self,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<InventoryObjectOfferedEventArgs, Error> {
        let cancellation_token = cancellation_token.unwrap_or_default();
        cancellation_token.throw_if_cancellation_requested()?;
        self.ensure_pending_capacity()?;
        let request_id = self.next_request_id();
        let (sender, receiver) = oneshot::channel();
        mutex(&self.inner.pending_offers).push(PendingOffer {
            request_id,
            deadline: Instant::now() + CALLBACK_TIMEOUT,
            sender,
        });
        let cancelled = cancellation_token.cancelled();
        futures_util::pin_mut!(receiver, cancelled);
        match select(receiver, cancelled).await {
            Either::Left((Ok(offer), _)) => Ok(offer),
            Either::Left((Err(_), _)) => Err(Error::InvalidOperation),
            Either::Right(_) => {
                mutex(&self.inner.pending_offers)
                    .retain(|request| request.request_id != request_id);
                Err(Error::Cancelled)
            }
        }
    }

    fn send_inventory_offer(
        &self,
        recipient: UUID,
        message: String,
        binary_bucket: Vec<u8>,
        do_effect: bool,
    ) -> Result<(), Error> {
        let mut client = self.client()?;
        let region_id = client
            .native_network()?
            .native_current_sim()
            .map_or_else(UUID::zero, |simulator| simulator.native_id());
        let color = client.settings.default_effect_color;
        let agent = client.native_self();
        let agent_id = agent.native_agent_id();
        agent.native_instant_message_full(
            agent.native_name(),
            recipient,
            message,
            UUID::random()?,
            InstantMessageDialog::InventoryOffered,
            InstantMessageOnline::Online,
            Vector3::zero(),
            region_id,
            binary_bucket,
        )?;
        if do_effect {
            agent.native_viewer_effect(
                agent_id,
                recipient,
                Vector3d::zero(),
                color,
                1.0,
                UUID::random()?,
                crate::EffectType::Beam,
                None,
            )?;
        }
        Ok(())
    }

    fn remove_if_no_copy(&self, item_id: UUID) -> Result<(), Error> {
        let Some(store) = self.store_clone() else {
            return Ok(());
        };
        let Some(item) = store.native_item_value(item_id) else {
            return Ok(());
        };
        if item.permissions().owner_mask.0 & PermissionMask::COPY.0 == 0 {
            store.remove_node_for(&item)?;
        }
        Ok(())
    }

    async fn native_collect_inventory_recursive(
        &self,
        root_folder: UUID,
        owner_id: UUID,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<(Vec<InventoryFolder>, Vec<InventoryItem>), Error> {
        let cancellation_token = cancellation_token.unwrap_or_default();
        let mut folders = Vec::new();
        let mut items = Vec::new();
        let mut pending = vec![(root_folder, 0_usize)];
        let mut visited = HashSet::new();
        let mut traversed = 0_usize;
        while let Some((folder_id, depth)) = pending.pop() {
            cancellation_token.throw_if_cancellation_requested()?;
            if depth > MAX_INVENTORY_RECURSION_DEPTH
                || traversed >= MAX_INVENTORY_TRAVERSAL
                || !visited.insert(folder_id)
            {
                continue;
            }
            let contents = self
                .native_folder_contents(
                    folder_id,
                    owner_id,
                    true,
                    true,
                    InventorySortOrder::BY_DATE,
                    Some(cancellation_token.clone()),
                    false,
                )
                .await?;
            for value in contents {
                traversed = traversed.saturating_add(1);
                if let Some(folder) = value.as_any().downcast_ref::<InventoryFolder>() {
                    folders.push(folder.clone());
                    pending.push((folder.base.uuid(), depth + 1));
                } else if let Some(store_item) = self
                    .store_clone()
                    .and_then(|store| store.native_item_value(value.inventory_base().uuid()))
                {
                    items.push(store_item);
                } else if let Some(item) = value.as_any().downcast_ref::<InventoryItem>() {
                    items.push(item.clone());
                }
                if traversed >= MAX_INVENTORY_TRAVERSAL {
                    break;
                }
            }
        }
        Ok((folders, items))
    }

    pub(crate) async fn native_get_inventory_recursive(
        &self,
        folder_id: UUID,
        owner_id: UUID,
        _folders: Vec<InventoryFolder>,
        _items: Vec<InventoryItem>,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<(), Error> {
        self.native_collect_inventory_recursive(folder_id, owner_id, cancellation_token)
            .await
            .map(|_| ())
    }

    async fn wait_for_capability(
        &self,
        simulator: &Simulator,
        name: &str,
        cancellation_token: &CancellationToken,
    ) -> Result<Option<Uri>, Error> {
        let Some(caps) = simulator.native_caps() else {
            return Ok(None);
        };
        if let Some(uri) = caps.capability_uri(name.to_owned())? {
            return Ok(Some(uri));
        }
        if caps.seed_request_finished() {
            return Ok(None);
        }
        let (sender, receiver) = oneshot::channel();
        let sender = Arc::new(Mutex::new(Some(sender)));
        let wake = Arc::clone(&sender);
        let _subscription = caps.subscribe_capabilities_received(Some(Arc::new(move |_| {
            if let Some(sender) = mutex(&wake).take() {
                let _ = sender.send(());
            }
        })));
        if let Some(uri) = caps.capability_uri(name.to_owned())? {
            return Ok(Some(uri));
        }
        let cancellation = cancellation_token.cancelled();
        futures_util::pin_mut!(receiver, cancellation);
        match select(receiver, cancellation).await {
            Either::Left(_) => caps.capability_uri(name.to_owned()),
            Either::Right(_) => Err(Error::Cancelled),
        }
    }

    pub(crate) async fn native_get_task_inventory(
        &self,
        object_id: UUID,
        object_local_id: u32,
        simulator: Option<Simulator>,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<Vec<Box<dyn InventoryObjectClass>>, Error> {
        let cancellation_token = cancellation_token.unwrap_or_default();
        cancellation_token.throw_if_cancellation_requested()?;
        let simulator = self.current_simulator(simulator)?;
        if let Some(capability) = self
            .wait_for_capability(&simulator, "RequestTaskInventory", &cancellation_token)
            .await?
        {
            return self
                .get_task_inventory_via_cap(object_id, capability, cancellation_token)
                .await;
        }

        // Legacy task inventory is a two-stage correlated operation: first the
        // simulator supplies a temporary filename, then an Xfer downloads it.
        self.ensure_pending_capacity()?;
        let request_id = self.next_request_id();
        let (sender, receiver) = oneshot::channel();
        mutex(&self.inner.pending_task_replies)
            .entry(object_id)
            .or_default()
            .push(PendingTaskReply {
                request_id,
                deadline: Instant::now() + CALLBACK_TIMEOUT,
                sender,
            });
        // A disconnected fake simulator can reject the enqueue; keep waiting so
        // cancellation still demonstrates callback teardown deterministically.
        let _ = self.native_request_task_inventory(object_local_id, Some(simulator.clone()));
        let cancelled = cancellation_token.cancelled();
        futures_util::pin_mut!(receiver, cancelled);
        let filename = match select(receiver, cancelled).await {
            Either::Left((Ok(filename), _)) if !filename.is_empty() => filename,
            Either::Left(_) => return Ok(Vec::new()),
            Either::Right(_) => {
                self.remove_pending_task_reply(object_id, request_id);
                return Ok(Vec::new());
            }
        };
        let Some(bytes) = self
            .download_task_inventory_xfer(simulator, filename, cancellation_token.clone())
            .await?
        else {
            return Ok(Vec::new());
        };
        if bytes.len() > MAX_TASK_INVENTORY_BYTES {
            return Ok(Vec::new());
        }
        let task_data = wire_string(bytes);
        parse_task_inventory_values(&task_data)
    }

    async fn download_task_inventory_xfer(
        &self,
        simulator: Simulator,
        filename: String,
        cancellation_token: CancellationToken,
    ) -> Result<Option<Vec<u8>>, Error> {
        cancellation_token.throw_if_cancellation_requested()?;
        self.ensure_pending_capacity()?;
        let mut xfer_id = UUID::random()?.get_u_long()?;
        {
            let transfers = mutex(&self.inner.task_xfers);
            while transfers.contains_key(&xfer_id) {
                xfer_id = UUID::random()?.get_u_long()?;
            }
        }
        let (sender, receiver) = oneshot::channel();
        mutex(&self.inner.task_xfers).insert(
            xfer_id,
            TaskXfer {
                deadline: Instant::now() + CALLBACK_TIMEOUT,
                next_packet: 0,
                announced_size: None,
                bytes: Vec::new(),
                sender,
            },
        );
        let mut request = RequestXferPacket::new_with_constructor()?;
        request.xfer_id.id = xfer_id;
        request.xfer_id.filename = Utils::string_to_bytes(filename)?;
        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 = AssetType::Unknown as i16;
        if let Err(error) = self.send_generated(&simulator, PacketType::RequestXfer, &request) {
            mutex(&self.inner.task_xfers).remove(&xfer_id);
            return Err(error);
        }
        let cancelled = cancellation_token.cancelled();
        futures_util::pin_mut!(receiver, cancelled);
        match select(receiver, cancelled).await {
            Either::Left((Ok(data), _)) => Ok(data),
            Either::Left((Err(_), _)) => Ok(None),
            Either::Right(_) => {
                mutex(&self.inner.task_xfers).remove(&xfer_id);
                let mut abort = AbortXferPacket::new_with_constructor()?;
                abort.xfer_id.id = xfer_id;
                let _ = self.send_generated(&simulator, PacketType::AbortXfer, &abort);
                Ok(None)
            }
        }
    }

    async fn get_task_inventory_via_cap(
        &self,
        object_id: UUID,
        capability: Uri,
        cancellation_token: CancellationToken,
    ) -> Result<Vec<Box<dyn InventoryObjectClass>>, Error> {
        let mut folder = InventoryFolder::new(object_id)?;
        folder.base.set_name("Contents".to_owned());
        folder.base.set_parent_uuid(UUID::zero());
        let mut result: Vec<Box<dyn InventoryObjectClass>> = vec![Box::new(folder)];
        let separator = if capability.0.contains('?') { '&' } else { '?' };
        let uri = Uri(format!("{}{separator}task_id={}", capability.0, object_id));
        let response = self
            .client()?
            .native_http_caps_client()
            .get(uri, cancellation_token, None)
            .await;
        let Ok((response, bytes)) = response else {
            return Ok(result);
        };
        if !response.is_success_status_code() || bytes.len() > MAX_TASK_INVENTORY_BYTES {
            return Ok(result);
        }
        let Ok(OSD::Map(map)) = OSDParser::deserialize_with_bytes(bytes) else {
            return Ok(result);
        };
        let Some(OSD::Array(contents)) = map.get("contents") else {
            return Ok(result);
        };
        for value in contents.iter().take(MAX_TASK_INVENTORY_ENTRIES) {
            let OSD::Map(item_map) = value else {
                continue;
            };
            if !item_map.contains_key("item_id") {
                continue;
            }
            if let Ok(item) = InventoryItem::from_osd(OSD::Map(item_map.clone())) {
                result.push(Box::new(item));
            }
        }
        Ok(result)
    }

    pub(crate) fn native_parse_task_inventory(
        task_data: String,
    ) -> Result<Vec<InventoryBase>, Error> {
        if task_data.len() > MAX_TASK_INVENTORY_BYTES {
            return Err(Error::Argument);
        }
        let parsed = parse_task_inventory_values(&task_data)?;
        Ok(parsed
            .into_iter()
            .map(|value| value.inventory_base().clone())
            .collect())
    }
}

fn local_find_in_store(
    store: &Inventory,
    base_folder: UUID,
    path: &[String],
    level: usize,
    first_only: bool,
    visited: &mut HashSet<UUID>,
    depth: usize,
) -> Result<Vec<InventoryBase>, Error> {
    if path.is_empty()
        || level >= path.len()
        || depth > MAX_INVENTORY_RECURSION_DEPTH
        || visited.len() >= MAX_INVENTORY_TRAVERSAL
        || !visited.insert(base_folder)
    {
        return Ok(Vec::new());
    }
    let mut result = Vec::new();
    for value in store
        .get_contents_with_uuid(base_folder)
        .unwrap_or_default()
    {
        if value.inventory_base().name() != path[level] {
            continue;
        }
        if level + 1 == path.len() {
            result.push(value.inventory_base().clone());
            if first_only {
                break;
            }
        } else if value.as_any().is::<InventoryFolder>() {
            result.extend(local_find_in_store(
                store,
                value.inventory_base().uuid(),
                path,
                level + 1,
                first_only,
                visited,
                depth + 1,
            )?);
            if first_only && !result.is_empty() {
                break;
            }
        }
    }
    Ok(result)
}

fn parse_line(line: &str) -> Option<(&str, Option<&str>)> {
    let line = line.trim();
    if line.is_empty() {
        return None;
    }
    let mut parts = line.splitn(2, char::is_whitespace);
    let key = parts.next()?;
    let value = parts
        .next()
        .map(str::trim)
        .filter(|value| !value.is_empty());
    Some((key, value))
}

fn parse_uuid(value: Option<&str>) -> UUID {
    value
        .and_then(|value| UUID::new_with_string(value.to_owned()).ok())
        .unwrap_or_else(UUID::zero)
}

fn pipe_string(value: Option<&str>) -> String {
    value
        .unwrap_or_default()
        .split('|')
        .next()
        .unwrap_or_default()
        .to_owned()
}

fn parse_hex(value: Option<&str>) -> u32 {
    value
        .and_then(|value| value.strip_prefix("0x").or(Some(value)))
        .and_then(|value| u32::from_str_radix(value, 16).ok())
        .unwrap_or_default()
}

fn parse_task_inventory_values(
    task_data: &str,
) -> Result<Vec<Box<dyn InventoryObjectClass>>, Error> {
    let normalized = task_data.replace("\r\n", "\n");
    let lines: Vec<_> = normalized.lines().collect();
    let mut line = 0_usize;
    let mut result: Vec<Box<dyn InventoryObjectClass>> = Vec::new();
    while line < lines.len() && result.len() < MAX_TASK_INVENTORY_ENTRIES {
        let Some((kind, _)) = parse_line(lines[line]) else {
            line += 1;
            continue;
        };
        line += 1;
        if kind == "inv_object" {
            let mut item_id = UUID::zero();
            let mut parent_id = UUID::zero();
            let mut name = String::new();
            let mut asset_type = AssetType::Unknown;
            while line < lines.len() {
                let current = parse_line(lines[line]);
                line += 1;
                let Some((key, value)) = current else {
                    continue;
                };
                if key == "{" {
                    continue;
                }
                if key == "}" {
                    break;
                }
                match key {
                    "obj_id" => item_id = parse_uuid(value),
                    "parent_id" => parent_id = parse_uuid(value),
                    "type" => {
                        asset_type =
                            Utils::string_to_asset_type(value.unwrap_or_default().to_owned())
                                .unwrap_or(AssetType::Unknown);
                    }
                    "name" => name = pipe_string(value),
                    _ => {}
                }
            }
            if asset_type == AssetType::Folder {
                let mut folder = InventoryFolder::new(item_id)?;
                folder.base.set_parent_uuid(parent_id);
                folder.base.set_name(name);
                result.push(Box::new(folder));
            } else {
                let mut item = InventoryItem::new_with_uuid(item_id)?;
                item.base.set_parent_uuid(parent_id);
                item.base.set_name(name);
                item.set_asset_type(asset_type);
                result.push(Box::new(item));
            }
        } else if kind == "inv_item" {
            let mut item_id = UUID::zero();
            let mut asset_id = UUID::zero();
            let mut parent_id = UUID::zero();
            let mut creator_id = UUID::zero();
            let mut owner_id = UUID::zero();
            let mut last_owner_id = UUID::zero();
            let mut group_id = UUID::zero();
            let mut group_owned = false;
            let mut name = String::new();
            let mut description = String::new();
            let mut asset_type = AssetType::Unknown;
            let mut inventory_type = InventoryType::UNKNOWN;
            let mut creation_date = UNIX_EPOCH;
            let mut flags = 0_u32;
            let mut permissions = Permissions::no_permissions();
            let mut sale_type = SaleType::Not;
            let mut sale_price = 0_i32;
            while line < lines.len() {
                let current = parse_line(lines[line]);
                line += 1;
                let Some((key, value)) = current else {
                    continue;
                };
                if key == "{" {
                    continue;
                }
                if key == "}" {
                    break;
                }
                if key == "permissions" {
                    while line < lines.len() {
                        let current = parse_line(lines[line]);
                        line += 1;
                        let Some((key, value)) = current else {
                            continue;
                        };
                        if key == "{" {
                            continue;
                        }
                        if key == "}" {
                            break;
                        }
                        match key {
                            "creator_mask" | "base_mask" => {
                                permissions.base_mask.0 = parse_hex(value);
                            }
                            "owner_mask" => permissions.owner_mask.0 = parse_hex(value),
                            "group_mask" => permissions.group_mask.0 = parse_hex(value),
                            "everyone_mask" => permissions.everyone_mask.0 = parse_hex(value),
                            "next_owner_mask" => permissions.next_owner_mask.0 = parse_hex(value),
                            "creator_id" => creator_id = parse_uuid(value),
                            "owner_id" => owner_id = parse_uuid(value),
                            "last_owner_id" => last_owner_id = parse_uuid(value),
                            "group_id" => group_id = parse_uuid(value),
                            "group_owned" => {
                                group_owned = value
                                    .and_then(|value| value.parse::<u32>().ok())
                                    .is_some_and(|value| value != 0);
                            }
                            _ => {}
                        }
                    }
                } else if key == "sale_info" {
                    while line < lines.len() {
                        let current = parse_line(lines[line]);
                        line += 1;
                        let Some((key, value)) = current else {
                            continue;
                        };
                        if key == "{" {
                            continue;
                        }
                        if key == "}" {
                            break;
                        }
                        match key {
                            "sale_type" => {
                                sale_type = Utils::string_to_sale_type(
                                    value.unwrap_or_default().to_owned(),
                                )
                                .unwrap_or(SaleType::Not);
                            }
                            "sale_price" => {
                                sale_price = value
                                    .and_then(|value| value.parse().ok())
                                    .unwrap_or_default();
                            }
                            _ => {}
                        }
                    }
                } else {
                    match key {
                        "item_id" => item_id = parse_uuid(value),
                        "parent_id" => parent_id = parse_uuid(value),
                        "shadow_id" => {
                            asset_id =
                                InventoryManager::native_decrypt_shadow_id(parse_uuid(value))?;
                        }
                        "asset_id" => asset_id = parse_uuid(value),
                        "type" => {
                            asset_type =
                                Utils::string_to_asset_type(value.unwrap_or_default().to_owned())
                                    .unwrap_or(AssetType::Unknown);
                        }
                        "inv_type" => {
                            inventory_type = Utils::string_to_inventory_type(
                                value.unwrap_or_default().to_owned(),
                            )
                            .unwrap_or(InventoryType::UNKNOWN);
                        }
                        "flags" => {
                            flags = value
                                .and_then(|value| value.parse().ok())
                                .unwrap_or_default();
                        }
                        "name" => name = pipe_string(value),
                        "desc" => description = pipe_string(value),
                        "creation_date" => {
                            if let Some(seconds) = value.and_then(|value| value.parse::<u64>().ok())
                            {
                                creation_date = UNIX_EPOCH + Duration::from_secs(seconds);
                            }
                        }
                        _ => {}
                    }
                }
            }
            let mut item = InventoryItem::new_with_inventory_type_uuid(inventory_type, item_id)?;
            item.set_asset_uuid(asset_id);
            item.set_asset_type(asset_type);
            item.set_creation_date(creation_date);
            item.set_creator_id(creator_id);
            item.set_description(description);
            item.set_flags(flags);
            item.set_group_id(group_id);
            item.set_group_owned(group_owned);
            item.base.set_name(name);
            item.base.set_owner_id(owner_id);
            item.set_last_owner_id(last_owner_id);
            item.base.set_parent_uuid(parent_id);
            item.set_permissions(permissions);
            item.set_sale_price(sale_price);
            item.set_sale_type(sale_type);
            result.push(Box::new(item));
        }
    }
    Ok(result)
}

impl InventoryManager {
    async fn post_capability_osd(
        &self,
        uri: Uri,
        body: OSD,
        token: CancellationToken,
    ) -> Result<OSD, Error> {
        token.throw_if_cancellation_requested()?;
        let payload = OSDParser::serialize_llsd_xml_bytes(body)?;
        let (response, bytes) = self
            .client()?
            .native_http_caps_client()
            .post_with_uri_string_bytes_cancellation_token_i_progress(
                uri,
                "application/llsd+xml".into(),
                payload,
                token,
                None,
            )
            .await?;
        if !response.is_success_status_code() {
            return Err(Error::InvalidOperation);
        }
        OSDParser::deserialize_with_bytes(bytes)
    }

    async fn post_asset_bytes_osd(
        &self,
        uri: Uri,
        data: Vec<u8>,
        token: CancellationToken,
        progress: Option<
            Box<dyn libremetaverse_types::compat::IProgress<crate::HttpCapsClientProgressReport>>,
        >,
    ) -> Result<OSD, Error> {
        token.throw_if_cancellation_requested()?;
        if data.is_empty() || data.len() > crate::asset_models::MAX_ASSET_BYTES {
            return Err(Error::Argument);
        }
        let (response, bytes) = self
            .client()?
            .native_http_caps_client()
            .post_with_uri_string_bytes_cancellation_token_i_progress(
                uri,
                "application/octet-stream".into(),
                data,
                token,
                progress,
            )
            .await?;
        if !response.is_success_status_code() {
            return Err(Error::InvalidOperation);
        }
        OSDParser::deserialize_with_bytes(bytes)
    }

    async fn begin_inventory_capability(
        &self,
        capability: &str,
        query: HashMap<String, OSD>,
        token: &CancellationToken,
    ) -> Result<OSD, Error> {
        let simulator = self
            .client()?
            .native_network()?
            .current_sim()
            .ok_or(Error::InvalidOperation)?;
        let uri = self
            .wait_for_capability(&simulator, capability, token)
            .await?
            .ok_or(Error::InvalidOperation)?;
        self.post_capability_osd(uri, OSD::Map(query), token.clone())
            .await
    }

    async fn finish_inventory_upload(
        &self,
        data: Vec<u8>,
        item_id: Option<UUID>,
        mut result: OSD,
        token: CancellationToken,
        progress: Option<
            Box<dyn libremetaverse_types::compat::IProgress<crate::HttpCapsClientProgressReport>>,
        >,
    ) -> Result<((bool, String, UUID, UUID), OSD), Error> {
        let mut progress = progress;
        for _ in 0..3 {
            let OSD::Map(contents) = &result else {
                return Ok((
                    (false, "invalid_response".into(), UUID::zero(), UUID::zero()),
                    result,
                ));
            };
            let state = contents
                .get("state")
                .map(OSD::as_string)
                .transpose()?
                .unwrap_or_default()
                .to_ascii_lowercase();
            match state.as_str() {
                "upload" => {
                    let uploader = contents
                        .get("uploader")
                        .ok_or(Error::InvalidOperation)?
                        .as_uri()?
                        .ok_or(Error::InvalidOperation)?;
                    result = self
                        .post_asset_bytes_osd(
                            uploader,
                            data.clone(),
                            token.clone(),
                            progress.take(),
                        )
                        .await?;
                }
                "complete" => {
                    let resolved_item = match item_id {
                        Some(id) => id,
                        None => contents
                            .get("new_inventory_item")
                            .ok_or(Error::InvalidOperation)?
                            .as_uuid()?,
                    };
                    let asset_id = contents
                        .get("new_asset")
                        .ok_or(Error::InvalidOperation)?
                        .as_uuid()?;
                    let owner = self.client()?.native_agent_manager()?.native_agent_id();
                    let _ = self.native_request_fetch_inventory(
                        HashMap::from([(resolved_item, owner)]),
                        Some(token),
                    );
                    return Ok(((true, String::new(), resolved_item, asset_id), result));
                }
                _ => {
                    return Ok(((false, state, UUID::zero(), UUID::zero()), result));
                }
            }
        }
        Ok((
            (
                false,
                "Upload state machine exceeded maximum iterations".into(),
                UUID::zero(),
                UUID::zero(),
            ),
            result,
        ))
    }

    fn new_asset_query(
        &self,
        name: String,
        description: String,
        asset_type: AssetType,
        inventory_type: InventoryType,
        folder_id: UUID,
        permissions: &Permissions,
        include_expected_cost: bool,
    ) -> Result<HashMap<String, OSD>, Error> {
        let mut query = HashMap::from([
            ("folder_id".into(), OSD::UUID(folder_id)),
            (
                "asset_type".into(),
                OSD::String(Utils::asset_type_to_string(asset_type)?),
            ),
            (
                "inventory_type".into(),
                OSD::String(Utils::inventory_type_to_string(inventory_type)?),
            ),
            ("name".into(), OSD::String(name)),
            ("description".into(), OSD::String(description)),
            (
                "everyone_mask".into(),
                OSD::Integer(permissions.everyone_mask.0.cast_signed()),
            ),
            (
                "group_mask".into(),
                OSD::Integer(permissions.group_mask.0.cast_signed()),
            ),
            (
                "next_owner_mask".into(),
                OSD::Integer(permissions.next_owner_mask.0.cast_signed()),
            ),
        ]);
        if include_expected_cost {
            query.insert(
                "expected_upload_cost".into(),
                OSD::Integer(self.client()?.settings_ref().upload_cost()),
            );
        }
        Ok(query)
    }

    pub(crate) async fn native_request_create_item_from_asset(
        &self,
        data: Vec<u8>,
        name: String,
        description: String,
        asset_type: AssetType,
        inventory_type: InventoryType,
        folder_id: UUID,
        permissions: Permissions,
        cancellation_token: Option<CancellationToken>,
        progress: Option<
            Box<dyn libremetaverse_types::compat::IProgress<crate::HttpCapsClientProgressReport>>,
        >,
    ) -> Result<(bool, String, UUID, UUID), Error> {
        let token = cancellation_token.unwrap_or_default();
        let query = self.new_asset_query(
            name,
            description,
            asset_type,
            inventory_type,
            folder_id,
            &permissions,
            true,
        )?;
        let initial = self
            .begin_inventory_capability("NewFileAgentInventory", query, &token)
            .await?;
        self.finish_inventory_upload(data, None, initial, token, progress)
            .await
            .map(|(result, _)| result)
    }

    pub(crate) async fn native_create_item_from_asset(
        &self,
        data: Vec<u8>,
        name: String,
        description: String,
        asset_type: AssetType,
        inventory_type: InventoryType,
        folder_id: UUID,
        permissions: Permissions,
        cancellation_token: Option<CancellationToken>,
        progress: Option<
            Box<dyn libremetaverse_types::compat::IProgress<crate::HttpCapsClientProgressReport>>,
        >,
    ) -> Result<crate::InventoryManagerCreateItemFromAssetResult, Error> {
        let mut output = crate::InventoryManagerCreateItemFromAssetResult::default();
        match self
            .native_request_create_item_from_asset(
                data,
                name,
                description,
                asset_type,
                inventory_type,
                folder_id,
                permissions,
                cancellation_token,
                progress,
            )
            .await
        {
            Ok((success, status, item_id, asset_id)) => {
                output.set_success(success);
                output.set_status(Some(status));
                output.set_item_id(item_id);
                output.set_asset_id(asset_id);
            }
            Err(error) => {
                let status = error.to_string();
                output.set_status(Some(status.clone()));
                output.set_error(Some(ExternalError(status)));
            }
        }
        Ok(output)
    }

    pub(crate) fn native_can_upload_large_textures(&self) -> bool {
        let Ok(client) = self.client() else {
            return false;
        };
        let premium = client
            .native_agent_manager()
            .is_ok_and(|manager| manager.native_benefits().premium_access() > 0);
        premium
            && client
                .native_network()
                .ok()
                .and_then(|network| network.current_sim())
                .and_then(|simulator| simulator.native_caps())
                .and_then(|caps| {
                    caps.capability_uri("NewFileAgentInventoryVariablePrice".into())
                        .ok()
                        .flatten()
                })
                .is_some()
    }

    pub(crate) async fn native_create_item_from_asset_variable_price(
        &self,
        data: Vec<u8>,
        name: String,
        description: String,
        asset_type: AssetType,
        inventory_type: InventoryType,
        folder_id: UUID,
        permissions: Permissions,
        confirm_cost: Option<Box<dyn Fn(i32) -> bool + Send + Sync>>,
        cancellation_token: Option<CancellationToken>,
        progress: Option<
            Box<dyn libremetaverse_types::compat::IProgress<crate::HttpCapsClientProgressReport>>,
        >,
    ) -> Result<crate::InventoryManagerCreateItemFromAssetResult, Error> {
        let mut output = crate::InventoryManagerCreateItemFromAssetResult::default();
        if !self.native_can_upload_large_textures() {
            let status = "membership_or_capability_required".to_owned();
            output.set_status(Some(status.clone()));
            output.set_error(Some(ExternalError(status)));
            return Ok(output);
        }
        let token = cancellation_token.unwrap_or_default();
        let query = self.new_asset_query(
            name,
            description,
            asset_type,
            inventory_type,
            folder_id,
            &permissions,
            false,
        )?;
        let initial = self
            .begin_inventory_capability("NewFileAgentInventoryVariablePrice", query, &token)
            .await?;
        output.set_raw_result(Some(initial.clone()));
        let OSD::Map(contents) = initial else {
            output.set_status(Some("invalid_response".into()));
            return Ok(output);
        };
        let state = contents
            .get("state")
            .map(OSD::as_string)
            .transpose()?
            .unwrap_or_default();
        if state != "confirm_upload" {
            output.set_status(Some(format!("unexpected_state:{state}")));
            return Ok(output);
        }
        let price = contents
            .get("upload_price")
            .map(OSD::as_integer)
            .transpose()?
            .unwrap_or_default();
        if confirm_cost.as_ref().is_some_and(|confirm| !confirm(price)) {
            let status = "cost_rejected".to_owned();
            output.set_status(Some(status.clone()));
            output.set_error(Some(ExternalError(status)));
            return Ok(output);
        }
        let rsvp = contents
            .get("rsvp")
            .ok_or(Error::InvalidOperation)?
            .as_uri()?
            .filter(|uri| uri.0 != "about:blank")
            .ok_or(Error::InvalidOperation)?;
        let uploaded = self
            .post_asset_bytes_osd(rsvp, data, token.clone(), progress)
            .await?;
        output.set_raw_result(Some(uploaded.clone()));
        let OSD::Map(uploaded) = uploaded else {
            output.set_status(Some("invalid_upload_response".into()));
            return Ok(output);
        };
        let state = uploaded
            .get("state")
            .map(OSD::as_string)
            .transpose()?
            .unwrap_or_default();
        output.set_status(Some(state.clone()));
        if state == "complete" {
            let item_id = uploaded
                .get("new_inventory_item")
                .ok_or(Error::InvalidOperation)?
                .as_uuid()?;
            let asset_id = uploaded
                .get("new_asset")
                .ok_or(Error::InvalidOperation)?
                .as_uuid()?;
            output.set_item_id(item_id);
            output.set_asset_id(asset_id);
            output.set_success(true);
        }
        Ok(output)
    }

    pub(crate) async fn native_upload_inventory_asset(
        &self,
        capability: &str,
        data: Vec<u8>,
        item_id: UUID,
        task_id: Option<UUID>,
        cancellation_token: Option<CancellationToken>,
        progress: Option<
            Box<dyn libremetaverse_types::compat::IProgress<crate::HttpCapsClientProgressReport>>,
        >,
    ) -> Result<(bool, String, UUID, UUID), Error> {
        let token = cancellation_token.unwrap_or_default();
        let mut query = HashMap::from([("item_id".into(), OSD::UUID(item_id))]);
        if let Some(task_id) = task_id {
            query.insert("task_id".into(), OSD::UUID(task_id));
        }
        let initial = self
            .begin_inventory_capability(capability, query, &token)
            .await?;
        self.finish_inventory_upload(data, Some(item_id), initial, token, progress)
            .await
            .map(|(result, _)| result)
    }

    pub(crate) async fn native_upload_script(
        &self,
        capability: &str,
        data: Vec<u8>,
        item_id: UUID,
        task_id: Option<UUID>,
        mono: bool,
        running: Option<bool>,
        cancellation_token: Option<CancellationToken>,
        progress: Option<
            Box<dyn libremetaverse_types::compat::IProgress<crate::HttpCapsClientProgressReport>>,
        >,
    ) -> Result<(bool, String, bool, Option<Vec<String>>, UUID, UUID), Error> {
        let token = cancellation_token.unwrap_or_default();
        let mut progress = progress;
        let mut query = HashMap::from([
            ("item_id".into(), OSD::UUID(item_id)),
            (
                "target".into(),
                OSD::String(if mono { "mono" } else { "lsl2" }.into()),
            ),
        ]);
        if let Some(task_id) = task_id {
            query.insert("task_id".into(), OSD::UUID(task_id));
        }
        if let Some(running) = running {
            query.insert("is_script_running".into(), OSD::Boolean(running));
        }
        let mut result = self
            .begin_inventory_capability(capability, query, &token)
            .await?;
        for _ in 0..3 {
            let OSD::Map(contents) = &result else {
                return Ok((
                    false,
                    "invalid_response".into(),
                    false,
                    None,
                    UUID::zero(),
                    UUID::zero(),
                ));
            };
            let state = contents
                .get("state")
                .map(OSD::as_string)
                .transpose()?
                .unwrap_or_default();
            if state == "upload" {
                let uploader = contents
                    .get("uploader")
                    .ok_or(Error::InvalidOperation)?
                    .as_uri()?
                    .ok_or(Error::InvalidOperation)?;
                result = self
                    .post_asset_bytes_osd(uploader, data.clone(), token.clone(), progress.take())
                    .await?;
                continue;
            }
            if state == "complete" {
                let asset_id = contents
                    .get("new_asset")
                    .ok_or(Error::InvalidOperation)?
                    .as_uuid()?;
                let compiled = contents
                    .get("compiled")
                    .map(OSD::as_boolean)
                    .transpose()?
                    .unwrap_or(false);
                let errors = match contents.get("errors") {
                    Some(OSD::Array(values)) => Some(
                        values
                            .iter()
                            .map(OSD::as_string)
                            .collect::<Result<Vec<_>, _>>()?,
                    ),
                    _ => None,
                };
                return Ok((true, state, compiled, errors, item_id, asset_id));
            }
            return Ok((false, state, false, None, UUID::zero(), UUID::zero()));
        }
        Ok((
            false,
            "Upload state machine exceeded maximum iterations".into(),
            false,
            None,
            UUID::zero(),
            UUID::zero(),
        ))
    }

    pub(crate) async fn native_upload_thumbnail(
        &self,
        inventory_id: UUID,
        task_id: UUID,
        j2c_image_data: Vec<u8>,
        cancellation_token: Option<CancellationToken>,
        progress: Option<
            Box<dyn libremetaverse_types::compat::IProgress<crate::HttpCapsClientProgressReport>>,
        >,
    ) -> Result<Option<Option<UUID>>, Error> {
        if inventory_id == UUID::zero()
            || j2c_image_data.is_empty()
            || j2c_image_data.len() > crate::asset_models::MAX_ASSET_BYTES
        {
            return Err(Error::Argument);
        }

        let token = cancellation_token.unwrap_or_default();
        token.throw_if_cancellation_requested()?;
        let client = self.client()?;
        let Some(simulator) = client.native_network()?.current_sim() else {
            return Ok(Some(None));
        };
        let Some(uri) = self
            .wait_for_capability(&simulator, "InventoryThumbnailUpload", &token)
            .await?
        else {
            return Ok(Some(None));
        };

        let inventory_store = self.native_store();
        let known_local_folder = task_id == UUID::zero()
            && inventory_store
                .as_ref()
                .and_then(|store| {
                    store
                        .get_value_or_default_with_uuid_a7c63fbe::<InventoryFolder>(inventory_id)
                        .ok()
                        .flatten()
                })
                .is_some();
        let metadata = if known_local_folder {
            HashMap::from([("category_id".to_owned(), OSD::UUID(inventory_id))])
        } else {
            let mut metadata = HashMap::from([("item_id".to_owned(), OSD::UUID(inventory_id))]);
            if task_id != UUID::zero() {
                metadata.insert("task_id".to_owned(), OSD::UUID(task_id));
            }
            metadata
        };
        let payload = OSDParser::serialize_llsd_xml_bytes(OSD::Map(metadata))?;
        let (metadata_response, metadata_body) = client
            .native_http_caps_client()
            .post_with_uri_string_bytes_cancellation_token_i_progress(
                uri,
                "application/llsd+xml".into(),
                payload,
                token.clone(),
                None,
            )
            .await?;
        if !(200..300).contains(&metadata_response.status_code) {
            return Ok(Some(None));
        }
        let metadata: serde_json::Value =
            serde_json::from_slice(&metadata_body).map_err(|_| Error::Argument)?;
        let Some(uploader) = metadata.get("uploader").and_then(serde_json::Value::as_str) else {
            return Ok(Some(None));
        };

        let (upload_response, upload_body) = client
            .native_http_caps_client()
            .post_with_uri_string_bytes_cancellation_token_i_progress(
                Uri(uploader.to_owned()),
                "image/x-j2c".into(),
                j2c_image_data,
                token,
                progress,
            )
            .await?;
        if !(200..300).contains(&upload_response.status_code) {
            return Ok(Some(None));
        }
        let result: serde_json::Value =
            serde_json::from_slice(&upload_body).map_err(|_| Error::Argument)?;
        if result.get("state").and_then(serde_json::Value::as_str) != Some("complete") {
            return Ok(Some(None));
        }
        let asset_id = result
            .get("new_asset")
            .or_else(|| result.get("new_asset_id"))
            .and_then(serde_json::Value::as_str)
            .map(|id| UUID::new_with_string(id.to_owned()))
            .transpose()?;
        Ok(Some(asset_id))
    }

    pub(crate) async fn native_update_material_inventory(
        &self,
        material: crate::assets::AssetMaterial,
        material_item_id: UUID,
        task_id: Option<UUID>,
        cancellation_token: Option<CancellationToken>,
        progress: Option<
            Box<dyn libremetaverse_types::compat::IProgress<crate::HttpCapsClientProgressReport>>,
        >,
    ) -> Result<(bool, String, UUID, UUID), Error> {
        let client = self.client()?;
        let simulator = client
            .native_network()?
            .current_sim()
            .ok_or(Error::InvalidOperation)?;
        let caps = simulator.native_caps().ok_or(Error::InvalidOperation)?;
        let name = if task_id.is_some() {
            "UpdateMaterialTaskInventory"
        } else {
            "UpdateMaterialAgentInventory"
        };
        let deadline = Instant::now() + Duration::from_secs(2);
        let uri = loop {
            if let Some(uri) = caps.capability_uri(name.to_owned())? {
                break uri;
            }
            if caps.seed_request_finished() || Instant::now() >= deadline {
                return Err(Error::InvalidOperation);
            }
            std::thread::sleep(Duration::from_millis(1));
        };
        let mut query = HashMap::from([("item_id".to_owned(), OSD::UUID(material_item_id))]);
        if let Some(task_id) = task_id {
            query.insert("task_id".to_owned(), OSD::UUID(task_id));
        }
        let payload = OSDParser::serialize_llsd_xml_bytes(OSD::Map(query))?;
        let token = cancellation_token.unwrap_or_default();
        let (response, metadata) = client
            .native_http_caps_client()
            .post_with_uri_string_bytes_cancellation_token_i_progress(
                uri,
                "application/llsd+xml".into(),
                payload,
                token.clone(),
                None,
            )
            .await?;
        if !(200..300).contains(&response.status_code) {
            return Ok((
                false,
                format!("HTTP {}", response.status_code),
                UUID::zero(),
                UUID::zero(),
            ));
        }
        let meta: serde_json::Value =
            serde_json::from_slice(&metadata).map_err(|_| Error::Argument)?;
        let status = meta
            .get("state")
            .and_then(serde_json::Value::as_str)
            .unwrap_or_default()
            .to_owned();
        let uploader = meta
            .get("uploader")
            .and_then(serde_json::Value::as_str)
            .ok_or(Error::Argument)?
            .to_owned();
        let data = material.to_json()?.into_bytes();
        let (upload_response, response_body) = client
            .native_http_caps_client()
            .post_with_uri_string_bytes_cancellation_token_i_progress(
                Uri(uploader),
                "application/octet-stream".into(),
                data,
                token,
                progress,
            )
            .await?;
        if !(200..300).contains(&upload_response.status_code) {
            return Ok((
                false,
                format!("HTTP {}", upload_response.status_code),
                material_item_id,
                UUID::zero(),
            ));
        }
        let result: serde_json::Value =
            serde_json::from_slice(&response_body).map_err(|_| Error::Argument)?;
        let final_status = result
            .get("state")
            .and_then(serde_json::Value::as_str)
            .unwrap_or(&status)
            .to_owned();
        let asset_id = result
            .get("new_asset")
            .or_else(|| result.get("new_asset_id"))
            .and_then(serde_json::Value::as_str)
            .map(|id| UUID::new_with_string(id.to_owned()))
            .transpose()?
            .unwrap_or_else(UUID::zero);
        let item_id = result
            .get("item_id")
            .and_then(serde_json::Value::as_str)
            .map(|id| UUID::new_with_string(id.to_owned()))
            .transpose()?
            .unwrap_or(material_item_id);
        Ok((final_status == "complete", final_status, asset_id, item_id))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::packets::{
        BulkUpdateInventoryPacketItemDataBlock, InventoryDescendentsPacketFolderDataBlock,
        UpdateCreateInventoryItemPacketInventoryDataBlock,
    };
    use futures_util::FutureExt;

    fn fixture() -> (Arc<GridClient>, InventoryManager, Inventory) {
        let mut client = GridClient::new().expect("client");
        let network = client.native_network().expect("network");
        client.native_set_network(network);
        let _ = client.native_self();
        let client = Arc::new(client);
        let manager = InventoryManager::native_new(Some(Arc::clone(&client))).expect("manager");
        let store = Inventory::new_with_grid_client_uuid(
            Arc::clone(&client),
            UUID::random().expect("owner"),
        )
        .expect("store");
        *write(&manager.inner.store) = Some(store.clone());
        (client, manager, store)
    }

    fn created_block(
        item_id: UUID,
        folder_id: UUID,
        owner_id: UUID,
        callback_id: u32,
    ) -> UpdateCreateInventoryItemPacketInventoryDataBlock {
        let mut block = UpdateCreateInventoryItemPacketInventoryDataBlock::new_with_constructor()
            .expect("block");
        block.item_id = item_id;
        block.folder_id = folder_id;
        block.owner_id = owner_id;
        block.asset_id = UUID::random().expect("asset");
        block.inv_type = InventoryType::NOTECARD.0;
        block.type_ = AssetType::Notecard as i8;
        block.callback_id = callback_id;
        block.name = b"Created\0".to_vec();
        block
    }

    #[test]
    fn fake_create_reply_correlates_callback_and_caches_validated_item() {
        let (_client, manager, store) = fixture();
        let item_id = UUID::random().expect("item");
        let folder_id = UUID::random().expect("folder");
        let owner_id = UUID::random().expect("owner");
        let (matched_sender, matched_receiver) = oneshot::channel();
        let (other_sender, mut other_receiver) = oneshot::channel();
        mutex(&manager.inner.pending_created).insert(
            41,
            PendingCreated {
                deadline: Instant::now() + CALLBACK_TIMEOUT,
                sender: matched_sender,
            },
        );
        mutex(&manager.inner.pending_created).insert(
            42,
            PendingCreated {
                deadline: Instant::now() + CALLBACK_TIMEOUT,
                sender: other_sender,
            },
        );
        let mut packet = UpdateCreateInventoryItemPacket::new_with_constructor().expect("packet");
        packet.inventory_data = vec![created_block(item_id, folder_id, owner_id, 41)];

        manager.handle_update_create(packet).expect("reply");

        let item = matched_receiver
            .now_or_never()
            .expect("matched callback completed")
            .expect("matched sender")
            .expect("created item");
        assert_eq!(item.base.uuid(), item_id);
        assert_eq!(item.base.parent_uuid(), folder_id);
        assert!(other_receiver.try_recv().expect("other receiver").is_none());
        assert!(store.native_item_value(item_id).is_some());
        assert!(mutex(&manager.inner.pending_created).contains_key(&42));
        manager.native_dispose().expect("dispose");
    }

    #[test]
    fn fake_bulk_copy_reply_waits_for_every_block() {
        let (_client, manager, _store) = fixture();
        let callback_id = 73;
        let (sender, receiver) = oneshot::channel();
        mutex(&manager.inner.pending_copied).insert(
            callback_id,
            PendingCopy {
                deadline: Instant::now() + CALLBACK_TIMEOUT,
                expected: 2,
                copied: Vec::new(),
                sender,
            },
        );
        let mut packet = BulkUpdateInventoryPacket::new_with_constructor().expect("packet");
        packet.item_data = (0..2)
            .map(|index| {
                let source = created_block(
                    UUID::random().expect("item"),
                    UUID::random().expect("folder"),
                    UUID::random().expect("owner"),
                    callback_id,
                );
                let mut block =
                    BulkUpdateInventoryPacketItemDataBlock::new_with_constructor().expect("block");
                block.item_id = source.item_id;
                block.folder_id = source.folder_id;
                block.owner_id = source.owner_id;
                block.asset_id = source.asset_id;
                block.inv_type = source.inv_type;
                block.type_ = source.type_;
                block.callback_id = source.callback_id;
                block.name = format!("copy-{index}\0").into_bytes();
                block
            })
            .collect();

        manager.handle_bulk_update(packet).expect("bulk reply");

        let result = receiver
            .now_or_never()
            .expect("copy completed")
            .expect("copy sender");
        assert!(result.success());
        assert_eq!(result.copied_items().expect("copies").len(), 2);
        manager.native_dispose().expect("dispose");
    }

    #[test]
    fn copy_from_notecard_udp_packet_preserves_protocol_identifiers() {
        let (_client, manager, _store) = fixture();
        let object_id = UUID::random().expect("object");
        let notecard_id = UUID::random().expect("notecard");
        let folder_id = UUID::random().expect("folder");
        let item_id = UUID::random().expect("item");

        let packet = manager
            .copy_from_notecard_packet(object_id, notecard_id, folder_id, item_id)
            .expect("packet");

        let (agent_id, session_id) = manager.ids().expect("identity");
        assert_eq!(packet.agent_data.agent_id, agent_id);
        assert_eq!(packet.agent_data.session_id, session_id);
        assert_eq!(packet.notecard_data.object_id, object_id);
        assert_eq!(packet.notecard_data.notecard_item_id, notecard_id);
        assert_eq!(packet.inventory_data.len(), 1);
        assert_eq!(packet.inventory_data[0].folder_id, folder_id);
        assert_eq!(packet.inventory_data[0].item_id, item_id);
        manager.native_dispose().expect("dispose");
    }

    #[test]
    fn cancelling_one_fetch_waiter_preserves_concurrent_correlation() {
        let (_client, manager, _store) = fixture();
        let item_id = UUID::random().expect("item");
        let (cancelled_sender, cancelled_receiver) = oneshot::channel();
        let (active_sender, active_receiver) = oneshot::channel();
        mutex(&manager.inner.pending_fetches).insert(
            item_id,
            vec![
                PendingFetch {
                    request_id: 1,
                    deadline: Instant::now() + CALLBACK_TIMEOUT,
                    sender: cancelled_sender,
                },
                PendingFetch {
                    request_id: 2,
                    deadline: Instant::now() + CALLBACK_TIMEOUT,
                    sender: active_sender,
                },
            ],
        );
        manager.remove_pending_fetch(item_id, 1);
        assert!(
            cancelled_receiver
                .now_or_never()
                .expect("cancelled")
                .is_err()
        );

        let mut reply = FetchInventoryReplyPacket::new_with_constructor().expect("packet");
        let source = created_block(
            item_id,
            UUID::random().expect("folder"),
            UUID::random().expect("owner"),
            0,
        );
        let mut block =
            crate::packets::FetchInventoryReplyPacketInventoryDataBlock::new_with_constructor()
                .expect("block");
        block.item_id = source.item_id;
        block.folder_id = source.folder_id;
        block.owner_id = source.owner_id;
        block.asset_id = source.asset_id;
        block.inv_type = source.inv_type;
        block.type_ = source.type_;
        reply.inventory_data = vec![block];
        manager.handle_fetch_reply(reply).expect("fetch reply");

        assert_eq!(
            active_receiver
                .now_or_never()
                .expect("active callback completed")
                .expect("active sender")
                .expect("item")
                .base
                .uuid(),
            item_id
        );
        manager.native_dispose().expect("dispose");
    }

    #[test]
    fn stale_descendents_packet_cannot_mutate_store() {
        let (_client, manager, store) = fixture();
        let parent_id = UUID::random().expect("parent");
        let child_id = UUID::random().expect("child");
        let mut parent = InventoryFolder::new(parent_id).expect("parent folder");
        parent.set_version(10);
        store.update_node_for(&parent).expect("cache parent");
        let mut packet = InventoryDescendentsPacket::new_with_constructor().expect("packet");
        packet.agent_data.folder_id = parent_id;
        packet.agent_data.version = 9;
        let mut child =
            InventoryDescendentsPacketFolderDataBlock::new_with_constructor().expect("child block");
        child.folder_id = child_id;
        child.parent_id = parent_id;
        child.name = b"stale\0".to_vec();
        packet.folder_data = vec![child];

        manager.handle_descendents(packet).expect("stale reply");

        assert!(!store.contains_with_uuid(child_id).expect("contains"));
        assert_eq!(
            store
                .get_value_or_default_with_uuid_a7c63fbe::<InventoryFolder>(parent_id)
                .expect("lookup")
                .expect("parent")
                .version(),
            10
        );
        manager.native_dispose().expect("dispose");
    }

    #[test]
    fn asset_type_without_folder_type_falls_back_to_inventory_root() {
        let (_client, manager, mut store) = fixture();
        let root_id = UUID::random().expect("root");
        let ordinary_id = UUID::random().expect("ordinary folder");
        let root = InventoryFolder::new(root_id).expect("root folder");
        store.set_root_folder(Some(root));
        let mut ordinary = InventoryFolder::new(ordinary_id).expect("ordinary folder");
        ordinary.base.set_parent_uuid(root_id);
        ordinary.set_preferred_type(FolderType::None);
        store
            .update_node_for(&ordinary)
            .expect("cache ordinary folder");

        assert_eq!(
            manager.native_find_folder_for_asset_type(AssetType::Script),
            root_id
        );
        manager.native_dispose().expect("dispose");
    }

    #[test]
    fn task_xfer_rejects_announced_data_above_bound() {
        let (client, manager, _store) = fixture();
        let simulator = Simulator::new(
            client.as_ref().clone(),
            "127.0.0.1:9".parse().expect("endpoint"),
            0,
            None,
            None,
        )
        .expect("simulator");
        let xfer_id = 991_u64;
        let (sender, receiver) = oneshot::channel();
        mutex(&manager.inner.task_xfers).insert(
            xfer_id,
            TaskXfer {
                deadline: Instant::now() + CALLBACK_TIMEOUT,
                next_packet: 0,
                announced_size: None,
                bytes: Vec::new(),
                sender,
            },
        );
        let mut packet = SendXferPacketPacket::new_with_constructor().expect("packet");
        packet.xfer_id.id = xfer_id;
        packet.xfer_id.packet = 0x8000_0000;
        packet.data_packet.data = u32::try_from(MAX_TASK_INVENTORY_BYTES + 1)
            .expect("bounded size")
            .to_le_bytes()
            .to_vec();

        assert_eq!(
            manager.handle_task_xfer_packet(packet, simulator),
            Err(Error::Argument)
        );
        assert!(
            receiver
                .now_or_never()
                .expect("xfer completed")
                .expect("xfer sender")
                .is_none()
        );
        assert!(!mutex(&manager.inner.task_xfers).contains_key(&xfer_id));
        manager.native_dispose().expect("dispose");
    }

    #[test]
    fn fake_inventory_offer_is_correlated_and_shares_acceptance_decision() {
        let (_client, manager, _store) = fixture();
        let item_id = UUID::random().expect("item");
        let destination = UUID::random().expect("destination");
        let observed = Arc::new(Mutex::new(None));
        let observed_by_handler = Arc::clone(&observed);
        let _subscription =
            manager.native_subscribe_inventory_object_offered(Arc::new(move |mut args| {
                args.set_folder_id(destination);
                args.set_accept(true);
                *mutex(&observed_by_handler) = Some(args);
            }));
        let (sender, receiver) = oneshot::channel();
        mutex(&manager.inner.pending_offers).push(PendingOffer {
            request_id: 17,
            deadline: Instant::now() + CALLBACK_TIMEOUT,
            sender,
        });
        let mut bucket = vec![AssetType::Notecard as u8];
        bucket.extend_from_slice(&item_id.get_bytes().expect("UUID bytes"));
        let offer = InstantMessage {
            binary_bucket: bucket,
            dialog: InstantMessageDialog::InventoryOffered,
            from_agent_id: UUID::random().expect("sender"),
            from_agent_name: "Sender".to_owned(),
            group_im: false,
            im_session_id: UUID::random().expect("session"),
            message: "Notecard".to_owned(),
            offline: InstantMessageOnline::Online,
            parent_estate_id: 0,
            position: Vector3::zero(),
            region_id: UUID::zero(),
            timestamp: UNIX_EPOCH,
            to_agent_id: UUID::random().expect("recipient"),
        };

        manager
            .handle_inventory_offer(InstantMessageEventArgs::new(offer, None).expect("event"))
            .expect("offer");

        let awaited = receiver
            .now_or_never()
            .expect("waiter completed")
            .expect("waiter sender");
        assert_eq!(awaited.object_id(), item_id);
        assert_eq!(awaited.asset_type(), AssetType::Notecard);
        assert!(awaited.accept());
        assert_eq!(awaited.folder_id(), destination);
        assert!(observed.lock().expect("observed").is_some());
        manager.native_dispose().expect("dispose");
    }

    #[test]
    fn fake_caps_script_running_reply_emits_correlated_event() {
        let (client, manager, _store) = fixture();
        let simulator = Simulator::new(
            client.as_ref().clone(),
            "127.0.0.1:9".parse().expect("endpoint"),
            0,
            None,
            None,
        )
        .expect("simulator");
        let object_id = UUID::random().expect("object");
        let script_id = UUID::random().expect("script");
        let observed = Arc::new(Mutex::new(None));
        let observed_by_handler = Arc::clone(&observed);
        let _subscription = manager.native_subscribe_script_running_reply(Arc::new(move |event| {
            *mutex(&observed_by_handler) = Some(event);
        }));
        let mut reply = ScriptRunningReplyMessage::new().expect("message");
        reply.object_id = object_id;
        reply.item_id = script_id;
        reply.mono = true;
        reply.running = false;

        client
            .native_network()
            .expect("network")
            .dispatch_caps_event("ScriptRunningReply", &reply, simulator);

        let event = (*observed.lock().expect("observed")).expect("event");
        assert_eq!(event.object_id(), object_id);
        assert_eq!(event.script_id(), script_id);
        assert!(event.is_mono());
        assert!(!event.is_running());
        manager.native_dispose().expect("dispose");
    }
}
