//! Native inventory models, hierarchy store, and bounded cache persistence.
//!
//! The store deliberately owns no transport behavior. Network inventory
//! managers can populate it, but all mutation, indexing, notifications, and
//! persistence remain deterministic local operations.

#![allow(clippy::missing_errors_doc)] // Public signatures mirror the mapped C# API.
#![allow(clippy::must_use_candidate)]
#![allow(clippy::needless_pass_by_value)]

use crate::agent_manager::EventRegistry;
use crate::{GridClient, InventoryItemFlags, InventoryObjectClass, PermissionMask};
use libremetaverse_structured_data::{OSD, OSDMap};
use libremetaverse_types::compat::{CancellationToken, EventHandler, Object, Subscription};
use libremetaverse_types::{
    AssetType, AttachmentPoint, Error, FolderType, InventoryType, SaleType, UUID, WearableType,
};
use std::collections::{HashMap, HashSet};
use std::fmt;
use std::fs::{self, File, OpenOptions};
use std::hash::{Hash, Hasher};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock, Weak};
use std::time::{Duration, SystemTime, UNIX_EPOCH};

const CACHE_MAGIC: &[u8; 8] = b"INVCACHE";
const CACHE_VERSION: u32 = 1;
const MAX_CACHE_BYTES: u64 = 64 * 1024 * 1024;
const MAX_CACHE_RECORDS: usize = 1_000_000;
const MAX_STRING_BYTES: usize = 1024 * 1024;
const MAX_HIERARCHY_DEPTH: usize = 512;

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)
}

#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct Permissions {
    pub base_mask: PermissionMask,
    pub everyone_mask: PermissionMask,
    pub group_mask: PermissionMask,
    pub next_owner_mask: PermissionMask,
    pub owner_mask: PermissionMask,
}

impl Default for Permissions {
    fn default() -> Self {
        Self::no_permissions()
    }
}

impl Permissions {
    pub fn full_permissions() -> Self {
        Self {
            base_mask: PermissionMask::ALL,
            everyone_mask: PermissionMask::ALL,
            group_mask: PermissionMask::ALL,
            next_owner_mask: PermissionMask::ALL,
            owner_mask: PermissionMask::ALL,
        }
    }

    pub const fn no_permissions() -> Self {
        Self {
            base_mask: PermissionMask::NONE,
            everyone_mask: PermissionMask::NONE,
            group_mask: PermissionMask::NONE,
            next_owner_mask: PermissionMask::NONE,
            owner_mask: PermissionMask::NONE,
        }
    }

    pub const fn new(
        base_mask: u32,
        everyone_mask: u32,
        group_mask: u32,
        next_owner_mask: u32,
        owner_mask: u32,
    ) -> Result<Self, Error> {
        Ok(Self {
            base_mask: PermissionMask(base_mask),
            everyone_mask: PermissionMask(everyone_mask),
            group_mask: PermissionMask(group_mask),
            next_owner_mask: PermissionMask(next_owner_mask),
            owner_mask: PermissionMask(owner_mask),
        })
    }

    pub fn equals_with_permissions(&self, other: Self) -> bool {
        *self == other
    }

    pub fn equals_with_object(&self, obj: Option<Object>) -> bool {
        obj.as_ref()
            .and_then(Object::downcast_ref::<Self>)
            .is_some_and(|other| self == other)
    }

    pub fn from_osd(llsd: OSD) -> Result<Self, Error> {
        let OSD::Map(map) = llsd else {
            return Ok(Self::default());
        };
        Ok(Self {
            base_mask: PermissionMask(osd_u32(&map, "base_mask")?),
            everyone_mask: PermissionMask(osd_u32(&map, "everyone_mask")?),
            group_mask: PermissionMask(osd_u32(&map, "group_mask")?),
            next_owner_mask: PermissionMask(osd_u32(&map, "next_owner_mask")?),
            owner_mask: PermissionMask(osd_u32(&map, "owner_mask")?),
        })
    }

    pub fn get_hash_code(&self) -> i32 {
        (self.base_mask.0
            ^ self.everyone_mask.0
            ^ self.group_mask.0
            ^ self.next_owner_mask.0
            ^ self.owner_mask.0)
            .cast_signed()
    }

    pub fn get_next_permissions(&self) -> Result<Self, Error> {
        let next = self.next_owner_mask.0;
        Self::new(
            self.base_mask.0 & next,
            self.everyone_mask.0 & next,
            self.group_mask.0 & next,
            next,
            self.owner_mask.0 & next,
        )
    }

    pub fn get_osd(&self) -> Result<OSD, Error> {
        Ok(OSD::Map(HashMap::from([
            (
                "base_mask".into(),
                OSD::Integer(self.base_mask.0.cast_signed()),
            ),
            (
                "everyone_mask".into(),
                OSD::Integer(self.everyone_mask.0.cast_signed()),
            ),
            (
                "group_mask".into(),
                OSD::Integer(self.group_mask.0.cast_signed()),
            ),
            (
                "next_owner_mask".into(),
                OSD::Integer(self.next_owner_mask.0.cast_signed()),
            ),
            (
                "owner_mask".into(),
                OSD::Integer(self.owner_mask.0.cast_signed()),
            ),
        ])))
    }

    pub fn has_permissions(
        perms: PermissionMask,
        check_perms: PermissionMask,
    ) -> Result<bool, Error> {
        Ok(perms.0 & check_perms.0 == check_perms.0)
    }

    #[allow(clippy::should_implement_trait)] // C# operator mapping requires this name.
    pub fn eq(lhs: Self, rhs: Self) -> bool {
        lhs == rhs
    }

    pub fn ne(lhs: Self, rhs: Self) -> bool {
        lhs != rhs
    }
}

impl fmt::Display for Permissions {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            formatter,
            "Base: {:?}, Everyone: {:?}, Group: {:?}, NextOwner: {:?}, Owner: {:?}",
            self.base_mask,
            self.everyone_mask,
            self.group_mask,
            self.next_owner_mask,
            self.owner_mask
        )
    }
}

#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct InventoryBase {
    uuid: UUID,
    parent_uuid: UUID,
    name: String,
    owner_id: UUID,
}

impl InventoryBase {
    fn new(uuid: UUID) -> Self {
        Self {
            uuid,
            parent_uuid: UUID::zero(),
            name: String::new(),
            owner_id: UUID::zero(),
        }
    }

    pub fn equals_with_inventory_base(&self, other: Self) -> bool {
        *self == other
    }

    pub fn equals_with_object(&self, obj: Option<Object>) -> bool {
        obj.as_ref()
            .and_then(Object::downcast_ref::<Self>)
            .is_some_and(|other| self == other)
    }

    pub fn get_hash_code(&self) -> i32 {
        self.uuid.get_hash_code()
    }

    pub fn get_osd(&self) -> Result<OSD, Error> {
        Ok(OSD::Map(HashMap::from([
            ("item_id".into(), OSD::UUID(self.uuid)),
            ("parent_id".into(), OSD::UUID(self.parent_uuid)),
            ("name".into(), OSD::String(self.name.clone())),
            ("owner_id".into(), OSD::UUID(self.owner_id)),
        ])))
    }

    pub fn name(&self) -> String {
        self.name.clone()
    }

    pub fn set_name(&mut self, value: String) {
        self.name = value;
    }

    pub const fn owner_id(&self) -> UUID {
        self.owner_id
    }

    pub fn set_owner_id(&mut self, value: UUID) {
        self.owner_id = value;
    }

    pub const fn parent_uuid(&self) -> UUID {
        self.parent_uuid
    }

    pub fn set_parent_uuid(&mut self, value: UUID) {
        self.parent_uuid = value;
    }

    pub const fn uuid(&self) -> UUID {
        self.uuid
    }

    pub fn set_uuid(&mut self, value: UUID) {
        self.uuid = value;
    }
}

#[derive(Clone, Debug)]
pub struct InventoryItem {
    pub base: InventoryBase,
    asset_uuid: UUID,
    permissions: Permissions,
    asset_type: AssetType,
    inventory_type: InventoryType,
    creator_id: UUID,
    description: String,
    group_id: UUID,
    group_owned: bool,
    sale_price: i32,
    sale_type: SaleType,
    flags: u32,
    creation_date: SystemTime,
    transaction_id: UUID,
    last_owner_id: UUID,
}

impl PartialEq for InventoryItem {
    fn eq(&self, other: &Self) -> bool {
        self.base == other.base
            && self.asset_type == other.asset_type
            && self.asset_uuid == other.asset_uuid
            && self.creation_date == other.creation_date
            && self.description == other.description
            && self.flags == other.flags
            && self.group_id == other.group_id
            && self.group_owned == other.group_owned
            && self.inventory_type == other.inventory_type
            && self.permissions == other.permissions
            && self.sale_price == other.sale_price
            && self.sale_type == other.sale_type
            && self.last_owner_id == other.last_owner_id
    }
}

impl Eq for InventoryItem {}

impl Hash for InventoryItem {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.base.hash(state);
        self.asset_uuid.hash(state);
        self.asset_type.hash(state);
        self.inventory_type.hash(state);
        self.description.hash(state);
        self.group_id.hash(state);
        self.group_owned.hash(state);
        self.sale_price.hash(state);
        self.sale_type.hash(state);
        self.flags.hash(state);
        self.creation_date.hash(state);
        self.last_owner_id.hash(state);
        self.permissions.hash(state);
    }
}

impl InventoryItem {
    fn with_type(inventory_type: InventoryType, uuid: UUID) -> Self {
        Self {
            base: InventoryBase::new(uuid),
            asset_uuid: UUID::zero(),
            permissions: Permissions::default(),
            asset_type: AssetType::Unknown,
            inventory_type,
            creator_id: UUID::zero(),
            description: String::new(),
            group_id: UUID::zero(),
            group_owned: false,
            sale_price: 0,
            sale_type: SaleType::Not,
            flags: 0,
            creation_date: UNIX_EPOCH,
            transaction_id: UUID::zero(),
            last_owner_id: UUID::zero(),
        }
    }

    pub fn new_with_inventory_type_uuid(
        type_: InventoryType,
        item_id: UUID,
    ) -> Result<Self, Error> {
        Ok(Self::with_type(type_, item_id))
    }

    pub fn new_with_uuid(uuid: UUID) -> Result<Self, Error> {
        Ok(Self::with_type(InventoryType::UNKNOWN, uuid))
    }

    pub fn equals_with_inventory_base(&self, other: InventoryBase) -> bool {
        self.base == other
    }

    pub fn equals_with_inventory_item(&self, other: Self) -> bool {
        *self == other
    }

    pub fn equals_with_object(&self, obj: Option<Object>) -> bool {
        obj.as_ref()
            .and_then(Object::downcast_ref::<Self>)
            .is_some_and(|other| self == other)
    }

    pub fn from_osd(data: OSD) -> Result<Self, Error> {
        let OSD::Map(map) = data else {
            return Err(Error::Argument);
        };
        let inventory_type = parse_inventory_type(map.get("inv_type"))?;
        let asset_type = parse_asset_type(map.get("type"))?;
        let corrected_type = if inventory_type == InventoryType::TEXTURE
            && matches!(asset_type, AssetType::Object | AssetType::Mesh)
        {
            InventoryType::ATTACHMENT
        } else {
            inventory_type
        };
        let mut item = Self::with_type(corrected_type, osd_uuid(&map, "item_id")?);
        item.apply_map(&map)?;
        Ok(item)
    }

    pub fn get_hash_code(&self) -> i32 {
        self.base.get_hash_code()
    }

    pub fn get_osd(&self) -> Result<OSD, Error> {
        let OSD::Map(mut permissions) = self.permissions.get_osd()? else {
            unreachable!("Permissions always serializes as a map");
        };
        permissions.insert("creator_id".into(), OSD::UUID(self.creator_id));
        permissions.insert("last_owner_id".into(), OSD::UUID(self.last_owner_id));
        permissions.insert("is_owner_group".into(), OSD::Boolean(self.group_owned));
        permissions.insert("group_id".into(), OSD::UUID(self.group_id));
        Ok(OSD::Map(HashMap::from([
            ("item_id".into(), OSD::UUID(self.base.uuid)),
            ("parent_id".into(), OSD::UUID(self.base.parent_uuid)),
            (
                "type".into(),
                OSD::Integer(i32::from(self.asset_type as i8)),
            ),
            (
                "inv_type".into(),
                OSD::Integer(i32::from(self.inventory_type.0)),
            ),
            ("flags".into(), OSD::Integer(self.flags.cast_signed())),
            ("name".into(), OSD::String(self.base.name.clone())),
            ("desc".into(), OSD::String(self.description.clone())),
            ("asset_id".into(), OSD::UUID(self.asset_uuid)),
            ("created_at".into(), OSD::Date(self.creation_date)),
            ("permissions".into(), OSD::Map(permissions)),
            (
                "sale_info".into(),
                OSD::Map(HashMap::from([
                    ("sale_price".into(), OSD::Integer(self.sale_price)),
                    (
                        "sale_type".into(),
                        OSD::Integer(i32::from(self.sale_type as u8)),
                    ),
                ])),
            ),
        ])))
    }

    pub fn is_link(&self) -> Result<bool, Error> {
        Ok(matches!(
            self.asset_type,
            AssetType::Link | AssetType::LinkFolder
        ))
    }

    pub fn update(&mut self, data: OSDMap) -> Result<(), Error> {
        self.apply_map(&data.snapshot())
    }

    fn apply_map(&mut self, map: &HashMap<String, OSD>) -> Result<(), Error> {
        if let Some(value) = map.get("item_id") {
            self.base.uuid = value.as_uuid()?;
        }
        if let Some(value) = map.get("parent_id") {
            self.base.parent_uuid = value.as_uuid()?;
        }
        if let Some(value) = map.get("agent_id") {
            self.base.owner_id = value.as_uuid()?;
        }
        if let Some(value) = map.get("name") {
            self.base.name = value.as_string()?;
        }
        if let Some(value) = map.get("desc") {
            self.description = value.as_string()?;
        }
        if let Some(value) = map.get("permissions") {
            self.permissions = Permissions::from_osd(value.clone())?;
            if let OSD::Map(perms) = value {
                self.base.owner_id =
                    optional_uuid(perms, "owner_id")?.unwrap_or(self.base.owner_id);
                self.creator_id = optional_uuid(perms, "creator_id")?.unwrap_or(self.creator_id);
                self.last_owner_id =
                    optional_uuid(perms, "last_owner_id")?.unwrap_or(self.last_owner_id);
                self.group_id = optional_uuid(perms, "group_id")?.unwrap_or(self.group_id);
                if let Some(group_owned) = perms.get("is_owner_group") {
                    self.group_owned = group_owned.as_boolean()?;
                }
            }
        }
        if let Some(OSD::Map(sale)) = map.get("sale_info") {
            self.sale_price = osd_i32(sale, "sale_price")?;
            self.sale_type = sale_type_from_i32(osd_i32(sale, "sale_type")?)?;
        }
        if let Some(value) = map.get("shadow_id") {
            self.asset_uuid = decrypt_shadow_id(value.as_uuid()?)?;
        }
        if let Some(value) = map.get("asset_id") {
            self.asset_uuid = value.as_uuid()?;
        }
        if let Some(value) = map.get("linked_id") {
            self.asset_uuid = value.as_uuid()?;
        }
        if let Some(value) = map.get("type") {
            let value = parse_asset_type(Some(value))?;
            if value != AssetType::Unknown {
                self.asset_type = value;
            }
        }
        if let Some(value) = map.get("inv_type") {
            let value = parse_inventory_type(Some(value))?;
            if value != InventoryType::UNKNOWN {
                self.inventory_type = value;
            }
        }
        if let Some(value) = map.get("flags") {
            self.flags = value.as_u_integer()?;
        }
        if let Some(value) = map.get("created_at") {
            self.creation_date = match value {
                OSD::Date(value) => *value,
                _ => UNIX_EPOCH + Duration::from_secs(u64::from(value.as_u_integer()?)),
            };
        }
        Ok(())
    }

    pub const fn asset_type(&self) -> AssetType {
        self.asset_type
    }
    pub fn set_asset_type(&mut self, value: AssetType) {
        self.asset_type = value;
    }
    pub const fn asset_uuid(&self) -> UUID {
        self.asset_uuid
    }
    pub fn set_asset_uuid(&mut self, value: UUID) {
        self.asset_uuid = value;
    }
    pub fn creation_date(&self) -> SystemTime {
        self.creation_date
    }
    pub fn set_creation_date(&mut self, value: SystemTime) {
        self.creation_date = value;
    }
    pub const fn creator_id(&self) -> UUID {
        self.creator_id
    }
    pub fn set_creator_id(&mut self, value: UUID) {
        self.creator_id = value;
    }
    pub fn description(&self) -> String {
        self.description.clone()
    }
    pub fn set_description(&mut self, value: String) {
        self.description = value;
    }
    pub const fn flags(&self) -> u32 {
        self.flags
    }
    pub fn set_flags(&mut self, value: u32) {
        self.flags = value;
    }
    pub const fn group_id(&self) -> UUID {
        self.group_id
    }
    pub fn set_group_id(&mut self, value: UUID) {
        self.group_id = value;
    }
    pub const fn group_owned(&self) -> bool {
        self.group_owned
    }
    pub fn set_group_owned(&mut self, value: bool) {
        self.group_owned = value;
    }
    pub const fn inventory_type(&self) -> InventoryType {
        self.inventory_type
    }
    pub fn set_inventory_type(&mut self, value: InventoryType) {
        self.inventory_type = value;
    }
    pub const fn last_owner_id(&self) -> UUID {
        self.last_owner_id
    }
    pub fn set_last_owner_id(&mut self, value: UUID) {
        self.last_owner_id = value;
    }
    pub const fn permissions(&self) -> Permissions {
        self.permissions
    }
    pub fn set_permissions(&mut self, value: Permissions) {
        self.permissions = value;
    }
    pub fn resolved_asset_id(&self) -> UUID {
        if self.is_link().unwrap_or(false) {
            UUID::zero()
        } else {
            self.asset_uuid
        }
    }
    pub fn resolved_item_id(&self) -> UUID {
        if self.is_link().unwrap_or(false) {
            self.asset_uuid
        } else {
            self.base.uuid
        }
    }
    pub const fn sale_price(&self) -> i32 {
        self.sale_price
    }
    pub fn set_sale_price(&mut self, value: i32) {
        self.sale_price = value;
    }
    pub const fn sale_type(&self) -> SaleType {
        self.sale_type
    }
    pub fn set_sale_type(&mut self, value: SaleType) {
        self.sale_type = value;
    }
    pub const fn transaction_id(&self) -> UUID {
        self.transaction_id
    }
    pub fn set_transaction_id(&mut self, value: UUID) {
        self.transaction_id = value;
    }
}

impl fmt::Display for InventoryItem {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            formatter,
            "{:?} {} ({} {}) '{}'/'{}' {}",
            self.asset_type,
            self.asset_uuid,
            inventory_type_name(self.inventory_type),
            self.base.uuid,
            self.base.name,
            self.description,
            self.permissions
        )
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct InventoryFolder {
    pub base: InventoryBase,
    descendent_count: i32,
    preferred_type: FolderType,
    version: i32,
}

impl InventoryFolder {
    pub const VERSION_UNKNOWN: i32 = -1;

    pub fn new(uuid: UUID) -> Result<Self, Error> {
        Ok(Self {
            base: InventoryBase::new(uuid),
            descendent_count: 0,
            preferred_type: FolderType::None,
            version: 1,
        })
    }

    pub fn equals_with_inventory_base(&self, other: InventoryBase) -> bool {
        self.base == other
    }
    pub fn equals_with_inventory_folder(&self, other: Self) -> bool {
        *self == other
    }
    pub fn equals_with_object(&self, obj: Option<Object>) -> bool {
        obj.as_ref()
            .and_then(Object::downcast_ref::<Self>)
            .is_some_and(|other| self == other)
    }
    pub fn from_osd(data: OSD) -> Result<Self, Error> {
        let OSD::Map(map) = data else {
            return Err(Error::Argument);
        };
        let id = if map.contains_key("category_id") {
            osd_uuid(&map, "category_id")?
        } else {
            osd_uuid(&map, "folder_id")?
        };
        let mut folder = Self::new(id)?;
        folder.version = Self::VERSION_UNKNOWN;
        folder.apply_map(&map)?;
        Ok(folder)
    }
    pub fn get_hash_code(&self) -> i32 {
        self.base.get_hash_code()
    }
    pub fn get_osd(&self) -> Result<OSD, Error> {
        Ok(OSD::Map(HashMap::from([
            ("item_id".into(), OSD::UUID(self.base.uuid)),
            ("version".into(), OSD::Integer(self.version)),
            ("parent_id".into(), OSD::UUID(self.base.parent_uuid)),
            (
                "type".into(),
                OSD::Integer(i32::from(self.preferred_type as i8)),
            ),
            ("name".into(), OSD::String(self.base.name.clone())),
        ])))
    }
    pub fn update(&mut self, data: OSDMap) -> Result<(), Error> {
        self.apply_map(&data.snapshot())
    }
    fn apply_map(&mut self, map: &HashMap<String, OSD>) -> Result<(), Error> {
        if let Some(value) = map.get("category_id") {
            self.base.uuid = value.as_uuid()?;
        }
        if let Some(value) = map.get("folder_id") {
            self.base.uuid = value.as_uuid()?;
        }
        if let Some(value) = map.get("version") {
            self.version = value.as_integer()?;
        }
        if let Some(value) = map.get("parent_id") {
            self.base.parent_uuid = value.as_uuid()?;
        }
        if let Some(value) = map.get("type_default").or_else(|| map.get("type")) {
            self.preferred_type = folder_type_from_i32(value.as_integer()?)?;
        }
        if let Some(value) = map.get("descendents") {
            self.descendent_count = value.as_integer()?;
        }
        if let Some(value) = map.get("owner_id").or_else(|| map.get("agent_id")) {
            self.base.owner_id = value.as_uuid()?;
        }
        if let Some(value) = map.get("name") {
            self.base.name = value.as_string()?;
        }
        Ok(())
    }
    pub const fn descendent_count(&self) -> i32 {
        self.descendent_count
    }
    pub fn set_descendent_count(&mut self, value: i32) {
        self.descendent_count = value;
    }
    pub const fn preferred_type(&self) -> FolderType {
        self.preferred_type
    }
    pub fn set_preferred_type(&mut self, value: FolderType) {
        self.preferred_type = value;
    }
    pub const fn version(&self) -> i32 {
        self.version
    }
    pub fn set_version(&mut self, value: i32) {
        self.version = value;
    }
}

impl fmt::Display for InventoryFolder {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(&self.base.name)
    }
}

macro_rules! simple_inventory_item {
    ($name:ident, $inventory_type:expr) => {
        #[derive(Clone, Debug, Eq, PartialEq)]
        pub struct $name {
            pub base: InventoryItem,
        }
        impl $name {
            pub fn new(uuid: UUID) -> Result<Self, Error> {
                Ok(Self {
                    base: InventoryItem::with_type($inventory_type, uuid),
                })
            }
        }
    };
}

simple_inventory_item!(InventoryAnimation, InventoryType::ANIMATION);
simple_inventory_item!(InventoryCallingCard, InventoryType::CALLING_CARD);
simple_inventory_item!(InventoryCategory, InventoryType::CATEGORY);
simple_inventory_item!(InventoryGesture, InventoryType::GESTURE);
simple_inventory_item!(InventoryLSL, InventoryType::LSL);
simple_inventory_item!(InventoryMaterial, InventoryType::MATERIAL);
simple_inventory_item!(InventoryNotecard, InventoryType::NOTECARD);
simple_inventory_item!(InventorySettings, InventoryType::SETTINGS);
simple_inventory_item!(InventorySnapshot, InventoryType::SNAPSHOT);
simple_inventory_item!(InventorySound, InventoryType::SOUND);
simple_inventory_item!(InventoryTexture, InventoryType::TEXTURE);

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct InventoryLandmark {
    pub base: InventoryItem,
}
impl InventoryLandmark {
    pub fn new(uuid: UUID) -> Result<Self, Error> {
        Ok(Self {
            base: InventoryItem::with_type(InventoryType::LANDMARK, uuid),
        })
    }
    pub const fn landmark_visited(&self) -> bool {
        self.base.flags & 1 != 0
    }
    pub fn set_landmark_visited(&mut self, value: bool) {
        if value {
            self.base.flags |= 1;
        } else {
            self.base.flags &= !1;
        }
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct InventoryObject {
    pub base: InventoryItem,
}
impl InventoryObject {
    pub fn new(uuid: UUID) -> Result<Self, Error> {
        Ok(Self {
            base: InventoryItem::with_type(InventoryType::OBJECT, uuid),
        })
    }
    pub const fn attach_point(&self) -> AttachmentPoint {
        attachment_point_from_u8(self.base.flags.to_le_bytes()[0])
    }
    pub fn set_attach_point(&mut self, value: AttachmentPoint) {
        self.base.flags = u32::from(value as u8) | (self.base.flags & 0xffff_ff00);
    }
    pub const fn item_flags(&self) -> InventoryItemFlags {
        InventoryItemFlags(self.base.flags & !0xff)
    }
    pub fn set_item_flags(&mut self, value: InventoryItemFlags) {
        self.base.flags = value.0 | (self.base.flags & 0xff);
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct InventoryAttachment {
    pub base: InventoryItem,
}
impl InventoryAttachment {
    pub fn new(uuid: UUID) -> Result<Self, Error> {
        Ok(Self {
            base: InventoryItem::with_type(InventoryType::ATTACHMENT, uuid),
        })
    }
    pub const fn attachment_point(&self) -> AttachmentPoint {
        attachment_point_from_u8(self.base.flags.to_le_bytes()[0])
    }
    pub fn set_attachment_point(&mut self, value: AttachmentPoint) {
        self.base.flags = u32::from(value as u8);
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct InventoryWearable {
    pub base: InventoryItem,
}
impl InventoryWearable {
    pub fn new(uuid: UUID) -> Result<Self, Error> {
        Ok(Self {
            base: InventoryItem::with_type(InventoryType::WEARABLE, uuid),
        })
    }
    pub const fn wearable_type(&self) -> WearableType {
        wearable_type_from_u8(self.base.flags.to_le_bytes()[0])
    }
    pub fn set_wearable_type(&mut self, value: WearableType) {
        self.base.flags = u32::from(value as u8);
    }
}

#[derive(Clone, Debug)]
enum InventoryValue {
    Base(InventoryBase),
    Folder(InventoryFolder),
    Item(InventoryItem),
    Animation(InventoryAnimation),
    Attachment(InventoryAttachment),
    CallingCard(InventoryCallingCard),
    Category(InventoryCategory),
    Gesture(InventoryGesture),
    Lsl(InventoryLSL),
    Landmark(InventoryLandmark),
    Material(InventoryMaterial),
    Notecard(InventoryNotecard),
    Object(InventoryObject),
    Settings(InventorySettings),
    Snapshot(InventorySnapshot),
    Sound(InventorySound),
    Texture(InventoryTexture),
    Wearable(InventoryWearable),
}

impl InventoryValue {
    fn from_item_typed(item: InventoryItem) -> Self {
        macro_rules! typed {
            ($kind:ty, $variant:ident) => {{
                let mut value = <$kind>::new(item.base.uuid)
                    .expect("known inventory item constructor cannot fail");
                value.base = item;
                Self::$variant(value)
            }};
        }
        match item.inventory_type {
            InventoryType::TEXTURE => typed!(InventoryTexture, Texture),
            InventoryType::SOUND => typed!(InventorySound, Sound),
            InventoryType::CALLING_CARD => typed!(InventoryCallingCard, CallingCard),
            InventoryType::LANDMARK => typed!(InventoryLandmark, Landmark),
            InventoryType::OBJECT => typed!(InventoryObject, Object),
            InventoryType::NOTECARD => typed!(InventoryNotecard, Notecard),
            InventoryType::CATEGORY => typed!(InventoryCategory, Category),
            InventoryType::LSL => typed!(InventoryLSL, Lsl),
            InventoryType::SNAPSHOT => typed!(InventorySnapshot, Snapshot),
            InventoryType::ATTACHMENT => typed!(InventoryAttachment, Attachment),
            InventoryType::WEARABLE => typed!(InventoryWearable, Wearable),
            InventoryType::ANIMATION => typed!(InventoryAnimation, Animation),
            InventoryType::GESTURE => typed!(InventoryGesture, Gesture),
            InventoryType::SETTINGS => typed!(InventorySettings, Settings),
            InventoryType::MATERIAL => typed!(InventoryMaterial, Material),
            _ => Self::Item(item),
        }
    }

    fn from_class(value: &dyn InventoryObjectClass) -> Option<Self> {
        macro_rules! downcast {
            ($type:ty, $variant:ident) => {
                if let Some(value) = value.as_any().downcast_ref::<$type>() {
                    return Some(Self::$variant(value.clone()));
                }
            };
        }
        downcast!(InventoryBase, Base);
        downcast!(InventoryFolder, Folder);
        downcast!(InventoryItem, Item);
        downcast!(InventoryAnimation, Animation);
        downcast!(InventoryAttachment, Attachment);
        downcast!(InventoryCallingCard, CallingCard);
        downcast!(InventoryCategory, Category);
        downcast!(InventoryGesture, Gesture);
        downcast!(InventoryLSL, Lsl);
        downcast!(InventoryLandmark, Landmark);
        downcast!(InventoryMaterial, Material);
        downcast!(InventoryNotecard, Notecard);
        downcast!(InventoryObject, Object);
        downcast!(InventorySettings, Settings);
        downcast!(InventorySnapshot, Snapshot);
        downcast!(InventorySound, Sound);
        downcast!(InventoryTexture, Texture);
        downcast!(InventoryWearable, Wearable);
        None
    }

    fn base(&self) -> &InventoryBase {
        match self {
            Self::Base(value) => value,
            Self::Folder(value) => &value.base,
            Self::Item(value) => &value.base,
            Self::Animation(value) => &value.base.base,
            Self::Attachment(value) => &value.base.base,
            Self::CallingCard(value) => &value.base.base,
            Self::Category(value) => &value.base.base,
            Self::Gesture(value) => &value.base.base,
            Self::Lsl(value) => &value.base.base,
            Self::Landmark(value) => &value.base.base,
            Self::Material(value) => &value.base.base,
            Self::Notecard(value) => &value.base.base,
            Self::Object(value) => &value.base.base,
            Self::Settings(value) => &value.base.base,
            Self::Snapshot(value) => &value.base.base,
            Self::Sound(value) => &value.base.base,
            Self::Texture(value) => &value.base.base,
            Self::Wearable(value) => &value.base.base,
        }
    }

    fn item(&self) -> Option<&InventoryItem> {
        match self {
            Self::Item(value) => Some(value),
            Self::Animation(value) => Some(&value.base),
            Self::Attachment(value) => Some(&value.base),
            Self::CallingCard(value) => Some(&value.base),
            Self::Category(value) => Some(&value.base),
            Self::Gesture(value) => Some(&value.base),
            Self::Lsl(value) => Some(&value.base),
            Self::Landmark(value) => Some(&value.base),
            Self::Material(value) => Some(&value.base),
            Self::Notecard(value) => Some(&value.base),
            Self::Object(value) => Some(&value.base),
            Self::Settings(value) => Some(&value.base),
            Self::Snapshot(value) => Some(&value.base),
            Self::Sound(value) => Some(&value.base),
            Self::Texture(value) => Some(&value.base),
            Self::Wearable(value) => Some(&value.base),
            Self::Base(_) | Self::Folder(_) => None,
        }
    }

    fn folder(&self) -> Option<&InventoryFolder> {
        match self {
            Self::Folder(value) => Some(value),
            _ => None,
        }
    }

    fn folder_mut(&mut self) -> Option<&mut InventoryFolder> {
        match self {
            Self::Folder(value) => Some(value),
            _ => None,
        }
    }

    fn boxed(&self) -> Box<dyn InventoryObjectClass> {
        match self {
            Self::Base(value) => Box::new(value.clone()),
            Self::Folder(value) => Box::new(value.clone()),
            Self::Item(value) => Box::new(value.clone()),
            Self::Animation(value) => Box::new(value.clone()),
            Self::Attachment(value) => Box::new(value.clone()),
            Self::CallingCard(value) => Box::new(value.clone()),
            Self::Category(value) => Box::new(value.clone()),
            Self::Gesture(value) => Box::new(value.clone()),
            Self::Lsl(value) => Box::new(value.clone()),
            Self::Landmark(value) => Box::new(value.clone()),
            Self::Material(value) => Box::new(value.clone()),
            Self::Notecard(value) => Box::new(value.clone()),
            Self::Object(value) => Box::new(value.clone()),
            Self::Settings(value) => Box::new(value.clone()),
            Self::Snapshot(value) => Box::new(value.clone()),
            Self::Sound(value) => Box::new(value.clone()),
            Self::Texture(value) => Box::new(value.clone()),
            Self::Wearable(value) => Box::new(value.clone()),
        }
    }

    fn clone_as<T: Clone + 'static>(&self) -> Option<T> {
        self.boxed().as_any().downcast_ref::<T>().cloned()
    }

    fn tag(&self) -> u8 {
        match self {
            Self::Base(_) => 0,
            Self::Folder(_) => 1,
            Self::Item(_) => 2,
            Self::Animation(_) => 3,
            Self::Attachment(_) => 4,
            Self::CallingCard(_) => 5,
            Self::Category(_) => 6,
            Self::Gesture(_) => 7,
            Self::Lsl(_) => 8,
            Self::Landmark(_) => 9,
            Self::Material(_) => 10,
            Self::Notecard(_) => 11,
            Self::Object(_) => 12,
            Self::Settings(_) => 13,
            Self::Snapshot(_) => 14,
            Self::Sound(_) => 15,
            Self::Texture(_) => 16,
            Self::Wearable(_) => 17,
        }
    }
}

#[derive(Clone)]
pub struct InventoryNode {
    inner: Arc<RwLock<InventoryNodeInner>>,
}

#[derive(Debug)]
struct InventoryNodeInner {
    data: Option<InventoryValue>,
    parent: Option<Weak<RwLock<InventoryNodeInner>>>,
    children: HashMap<UUID, InventoryNode>,
    needs_update: bool,
}

impl fmt::Debug for InventoryNode {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        let inner = read(&self.inner);
        formatter
            .debug_struct("InventoryNode")
            .field("uuid", &inner.data.as_ref().map(|value| value.base().uuid))
            .field("children", &inner.children.len())
            .field("needs_update", &inner.needs_update)
            .finish()
    }
}

impl InventoryNode {
    fn empty() -> Self {
        Self {
            inner: Arc::new(RwLock::new(InventoryNodeInner {
                data: None,
                parent: None,
                children: HashMap::new(),
                needs_update: true,
            })),
        }
    }

    fn from_value(data: InventoryValue) -> Self {
        let node = Self::empty();
        write(&node.inner).data = Some(data);
        node
    }

    pub fn new_with_constructor() -> Result<Self, Error> {
        Ok(Self::empty())
    }

    pub fn new_with_inventory_base(data: &dyn InventoryObjectClass) -> Result<Self, Error> {
        Ok(Self::from_value(
            InventoryValue::from_class(data).ok_or(Error::Argument)?,
        ))
    }

    pub fn new_with_inventory_base_inventory_node(
        data: InventoryBase,
        parent: Self,
    ) -> Result<Self, Error> {
        let mut node = Self::from_value(InventoryValue::Base(data));
        node.set_parent(Some(parent.clone()));
        let uuid = node.uuid().ok_or(Error::Argument)?;
        write(&parent.inner).children.insert(uuid, node.clone());
        Ok(node)
    }

    fn uuid(&self) -> Option<UUID> {
        read(&self.inner)
            .data
            .as_ref()
            .map(|value| value.base().uuid)
    }

    fn value(&self) -> Option<InventoryValue> {
        read(&self.inner).data.clone()
    }

    fn set_value(&self, value: InventoryValue) {
        write(&self.inner).data = Some(value);
    }

    pub fn data(&self) -> Option<Box<dyn InventoryObjectClass>> {
        self.value().map(|value| value.boxed())
    }

    pub fn set_data(&mut self, value: Option<Box<dyn InventoryObjectClass>>) {
        write(&self.inner).data = value.as_deref().and_then(InventoryValue::from_class);
    }

    pub fn modify_time(&self) -> SystemTime {
        let inner = read(&self.inner);
        let Some(data) = inner.data.as_ref() else {
            return UNIX_EPOCH;
        };
        if let Some(item) = data.item() {
            return item.creation_date;
        }
        if data.folder().is_some() {
            let children: Vec<_> = inner.children.values().cloned().collect();
            drop(inner);
            return children
                .into_iter()
                .filter_map(|child| child.value())
                .filter_map(|value| value.item().map(InventoryItem::creation_date))
                .max()
                .unwrap_or(UNIX_EPOCH);
        }
        UNIX_EPOCH
    }

    pub fn needs_update(&self) -> bool {
        read(&self.inner).needs_update
    }
    pub fn set_needs_update(&mut self, value: bool) {
        write(&self.inner).needs_update = value;
    }
    pub fn nodes(&self) -> InventoryNodeDictionary {
        InventoryNodeDictionary {
            parent: self.clone(),
        }
    }
    pub fn set_nodes(&mut self, value: InventoryNodeDictionary) {
        let children = read(&value.parent.inner).children.clone();
        write(&self.inner).children = children;
        self.reparent_children();
    }
    pub fn parent(&self) -> Option<Self> {
        read(&self.inner)
            .parent
            .as_ref()
            .and_then(Weak::upgrade)
            .map(|inner| Self { inner })
    }
    pub fn set_parent(&mut self, value: Option<Self>) {
        write(&self.inner).parent = value.as_ref().map(|parent| Arc::downgrade(&parent.inner));
    }
    fn reparent_children(&self) {
        let children: Vec<_> = read(&self.inner).children.values().cloned().collect();
        for mut child in children {
            child.set_parent(Some(self.clone()));
        }
    }
}

impl fmt::Display for InventoryNode {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        let Some(value) = self.value() else {
            return formatter.write_str("[Empty Node]");
        };
        if let Some(folder) = value.folder() {
            write!(formatter, "{folder}")
        } else if let Some(item) = value.item() {
            write!(formatter, "{item}")
        } else {
            formatter.write_str(&value.base().name())
        }
    }
}

#[derive(Clone, Debug)]
pub struct InventoryNodeDictionary {
    parent: InventoryNode,
}
impl InventoryNodeDictionary {
    pub fn new(parent: InventoryNode) -> Result<Self, Error> {
        Ok(Self { parent })
    }
    pub fn add(&self, key: UUID, mut value: InventoryNode) -> Result<(), Error> {
        value.set_parent(Some(self.parent.clone()));
        write(&self.parent.inner).children.insert(key, value);
        Ok(())
    }
    pub fn contains(&self, key: UUID) -> Result<bool, Error> {
        Ok(read(&self.parent.inner).children.contains_key(&key))
    }
    pub fn remove(&self, key: UUID) -> Result<(), Error> {
        if let Some(mut removed) = write(&self.parent.inner).children.remove(&key) {
            removed.set_parent(None);
        }
        Ok(())
    }
    pub fn count(&self) -> i32 {
        i32::try_from(read(&self.parent.inner).children.len()).unwrap_or(i32::MAX)
    }
    pub fn item(&self, key: UUID) -> InventoryNode {
        read(&self.parent.inner).children[&key].clone()
    }
    pub fn set_item(&mut self, key: UUID, value: InventoryNode) {
        let _ = self.add(key, value);
    }
    pub fn keys(&self) -> Vec<UUID> {
        read(&self.parent.inner).children.keys().copied().collect()
    }
    pub fn parent(&self) -> InventoryNode {
        self.parent.clone()
    }
    pub fn set_parent(&mut self, value: InventoryNode) {
        self.parent = value;
        self.parent.reparent_children();
    }
    pub fn sync_root(&self) -> Object {
        Object::opaque(self.clone())
    }
    pub fn values(&self) -> Vec<InventoryNode> {
        read(&self.parent.inner)
            .children
            .values()
            .cloned()
            .collect()
    }
}

#[derive(Clone, Debug)]
pub struct InventoryObjectAddedEventArgs {
    value: InventoryValue,
}
impl InventoryObjectAddedEventArgs {
    pub fn new(obj: InventoryBase) -> Result<Self, Error> {
        Ok(Self {
            value: InventoryValue::Base(obj),
        })
    }
    fn from_value(value: InventoryValue) -> Self {
        Self { value }
    }
    pub fn obj(&self) -> InventoryBase {
        self.value.base().clone()
    }
    pub fn object(&self) -> Box<dyn InventoryObjectClass> {
        self.value.boxed()
    }
}

#[derive(Clone, Debug)]
pub struct InventoryObjectRemovedEventArgs {
    value: InventoryValue,
}
impl InventoryObjectRemovedEventArgs {
    pub fn new(obj: InventoryBase) -> Result<Self, Error> {
        Ok(Self {
            value: InventoryValue::Base(obj),
        })
    }
    fn from_value(value: InventoryValue) -> Self {
        Self { value }
    }
    pub fn obj(&self) -> InventoryBase {
        self.value.base().clone()
    }
    pub fn object(&self) -> Box<dyn InventoryObjectClass> {
        self.value.boxed()
    }
}

#[derive(Clone, Debug)]
pub struct InventoryObjectUpdatedEventArgs {
    old: InventoryValue,
    new: InventoryValue,
}
impl InventoryObjectUpdatedEventArgs {
    pub fn new(old_object: InventoryBase, new_object: InventoryBase) -> Result<Self, Error> {
        Ok(Self {
            old: InventoryValue::Base(old_object),
            new: InventoryValue::Base(new_object),
        })
    }
    fn from_values(old: InventoryValue, new: InventoryValue) -> Self {
        Self { old, new }
    }
    pub fn old_object(&self) -> InventoryBase {
        self.old.base().clone()
    }
    pub fn new_object(&self) -> InventoryBase {
        self.new.base().clone()
    }
    pub fn old_value(&self) -> Box<dyn InventoryObjectClass> {
        self.old.boxed()
    }
    pub fn new_value(&self) -> Box<dyn InventoryObjectClass> {
        self.new.boxed()
    }
}

#[derive(Default)]
struct InventoryState {
    items: HashMap<UUID, InventoryNode>,
    links: HashMap<UUID, HashSet<UUID>>,
    root: Option<UUID>,
    library_root: Option<UUID>,
    root_node: Option<InventoryNode>,
    library_root_node: Option<InventoryNode>,
}

struct InventoryInner {
    owner: UUID,
    _client: Weak<GridClient>,
    state: RwLock<InventoryState>,
    added: EventRegistry<InventoryObjectAddedEventArgs>,
    removed: EventRegistry<InventoryObjectRemovedEventArgs>,
    updated: EventRegistry<InventoryObjectUpdatedEventArgs>,
}

#[derive(Clone)]
pub struct Inventory {
    inner: Arc<InventoryInner>,
}

impl fmt::Debug for Inventory {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("Inventory")
            .field("owner", &self.owner())
            .field("count", &self.count())
            .finish()
    }
}

impl Inventory {
    pub fn subscribe_inventory_object_added(
        &self,
        handler: EventHandler<InventoryObjectAddedEventArgs>,
    ) -> Subscription {
        self.inner.added.subscribe(handler)
    }
    pub fn subscribe_inventory_object_removed(
        &self,
        handler: EventHandler<InventoryObjectRemovedEventArgs>,
    ) -> Subscription {
        self.inner.removed.subscribe(handler)
    }
    pub fn subscribe_inventory_object_updated(
        &self,
        handler: EventHandler<InventoryObjectUpdatedEventArgs>,
    ) -> Subscription {
        self.inner.updated.subscribe(handler)
    }

    pub fn new_with_grid_client(client: Arc<GridClient>) -> Result<Self, Error> {
        let owner = client.network().native_agent_id();
        Self::new_with_grid_client_uuid(client, owner)
    }

    pub fn new_with_grid_client_uuid(client: Arc<GridClient>, owner: UUID) -> Result<Self, Error> {
        Ok(Self {
            inner: Arc::new(InventoryInner {
                owner,
                _client: Arc::downgrade(&client),
                state: RwLock::new(InventoryState::default()),
                added: EventRegistry::default(),
                removed: EventRegistry::default(),
                updated: EventRegistry::default(),
            }),
        })
    }

    pub fn clear(&self) -> Result<(), Error> {
        let mut state = write(&self.inner.state);
        state.items.clear();
        state.links.clear();
        Ok(())
    }
    pub fn contains_with_inventory_base(
        &self,
        obj: &dyn InventoryObjectClass,
    ) -> Result<bool, Error> {
        self.contains_with_uuid(obj.inventory_base().uuid)
    }
    pub fn contains_with_uuid(&self, uuid: UUID) -> Result<bool, Error> {
        Ok(read(&self.inner.state).items.contains_key(&uuid))
    }

    pub fn find_all_links(&self, asset_id: UUID) -> Result<Vec<InventoryNode>, Error> {
        if asset_id == UUID::zero() {
            return Ok(Vec::new());
        }
        let state = read(&self.inner.state);
        Ok(state
            .links
            .get(&asset_id)
            .into_iter()
            .flatten()
            .filter_map(|uuid| state.items.get(uuid).cloned())
            .collect())
    }

    pub fn get_contents_with_inventory_folder(
        &self,
        folder: &InventoryFolder,
    ) -> Result<Vec<Box<dyn InventoryObjectClass>>, Error> {
        self.get_contents_with_uuid(folder.base.uuid)
    }
    pub fn get_contents_with_uuid(
        &self,
        folder: UUID,
    ) -> Result<Vec<Box<dyn InventoryObjectClass>>, Error> {
        let state = read(&self.inner.state);
        let node = state.items.get(&folder).ok_or(Error::InvalidOperation)?;
        if node
            .value()
            .and_then(|value| value.folder().cloned())
            .is_none()
        {
            return Err(Error::InvalidOperation);
        }
        Ok(read(&node.inner)
            .children
            .values()
            .filter_map(InventoryNode::value)
            .map(|value| value.boxed())
            .collect())
    }

    pub fn get_contents_sorted_with_uuid(
        &self,
        folder: UUID,
        order: crate::InventorySortOrder,
    ) -> Result<Vec<Box<dyn InventoryObjectClass>>, Error> {
        let mut values = self.get_contents_with_uuid(folder)?;
        values.sort_by(|left, right| compare_inventory(left.as_ref(), right.as_ref(), order));
        Ok(values)
    }

    pub fn find_folder_for_type(&self, folder_type: FolderType) -> Option<UUID> {
        let state = read(&self.inner.state);
        let root = state.root?;
        state
            .items
            .values()
            .filter_map(InventoryNode::value)
            .filter_map(|value| value.folder().cloned())
            .find(|folder| folder.base.parent_uuid == root && folder.preferred_type == folder_type)
            .map(|folder| folder.base.uuid)
    }

    pub fn get_node_for(&self, uuid: UUID) -> Result<InventoryNode, Error> {
        read(&self.inner.state)
            .items
            .get(&uuid)
            .cloned()
            .ok_or(Error::InvalidOperation)
    }
    pub fn get_node_or_default(&self, uuid: UUID) -> Result<Option<InventoryNode>, Error> {
        Ok(read(&self.inner.state).items.get(&uuid).cloned())
    }
    pub fn try_get_node_for(&self, uuid: UUID, node: &mut Option<InventoryNode>) -> bool {
        *node = read(&self.inner.state).items.get(&uuid).cloned();
        node.is_some()
    }

    pub fn get_value_or_default_with_uuid(
        &self,
        uuid: UUID,
    ) -> Result<Option<InventoryBase>, Error> {
        Ok(read(&self.inner.state)
            .items
            .get(&uuid)
            .and_then(InventoryNode::value)
            .map(|value| value.base().clone()))
    }
    pub fn get_value_or_default_with_uuid_a7c63fbe<T: Clone + 'static>(
        &self,
        uuid: UUID,
    ) -> Result<Option<T>, Error> {
        Ok(read(&self.inner.state)
            .items
            .get(&uuid)
            .and_then(InventoryNode::value)
            .and_then(|value| value.clone_as::<T>()))
    }

    /// Returns the shared item portion regardless of the concrete inventory
    /// subclass stored for the UUID.
    pub(crate) fn native_item_value(&self, uuid: UUID) -> Option<InventoryItem> {
        read(&self.inner.state)
            .items
            .get(&uuid)
            .and_then(InventoryNode::value)
            .and_then(|value| value.item().cloned())
    }

    /// Atomically applies one fully parsed AIS response. A detached candidate
    /// graph is built first, so validation or rebuilding failures cannot expose
    /// a partially reconciled inventory to readers.
    #[allow(clippy::too_many_lines)] // One lock-free candidate build followed by one state swap.
    pub(crate) fn native_apply_ais_batch(
        &self,
        folders: Vec<InventoryFolder>,
        items: Vec<InventoryItem>,
        removed_items: &[UUID],
        removed_categories: &[UUID],
        broken_links: &[UUID],
        versions: &HashMap<UUID, i32>,
    ) -> Result<(), Error> {
        let mut incoming = HashSet::new();
        for uuid in folders
            .iter()
            .map(|folder| folder.base.uuid)
            .chain(items.iter().map(|item| item.base.uuid))
        {
            if uuid == UUID::zero() || !incoming.insert(uuid) {
                return Err(Error::Argument);
            }
        }
        if incoming.len() > MAX_CACHE_RECORDS || versions.keys().any(|uuid| *uuid == UUID::zero()) {
            return Err(Error::Argument);
        }
        let mut touched = incoming.clone();
        touched.extend(versions.keys().copied());

        let (mut candidate, old_values) = {
            let state = read(&self.inner.state);
            let old_values: HashMap<_, _> = state
                .items
                .iter()
                .filter_map(|(uuid, node)| node.value().map(|value| (*uuid, value)))
                .collect();
            let mut candidate = InventoryState {
                root: state.root,
                library_root: state.library_root,
                ..InventoryState::default()
            };
            for (uuid, value) in &old_values {
                candidate
                    .items
                    .insert(*uuid, InventoryNode::from_value(value.clone()));
            }
            (candidate, old_values)
        };

        for folder in folders {
            candidate.items.insert(
                folder.base.uuid,
                InventoryNode::from_value(InventoryValue::Folder(folder)),
            );
        }
        for item in items {
            let uuid = item.base.uuid;
            let value = InventoryValue::from_item_typed(item);
            candidate
                .items
                .insert(uuid, InventoryNode::from_value(value));
        }
        for (uuid, version) in versions {
            if let Some(node) = candidate.items.get(uuid) {
                let Some(mut value) = node.value() else {
                    return Err(Error::InvalidOperation);
                };
                let Some(folder) = value.folder_mut() else {
                    return Err(Error::InvalidOperation);
                };
                folder.version = *version;
                node.set_value(value);
            }
        }

        let direct_removals: HashSet<_> = removed_items
            .iter()
            .chain(broken_links)
            .copied()
            .filter(|uuid| *uuid != UUID::zero())
            .collect();
        for uuid in direct_removals {
            candidate.items.remove(&uuid);
        }
        rebuild_indexes_and_counts(&mut candidate);
        for category in removed_categories
            .iter()
            .copied()
            .filter(|uuid| *uuid != UUID::zero())
        {
            let mut pending = vec![category];
            let mut descendants = HashSet::new();
            while let Some(uuid) = pending.pop() {
                if descendants.len() >= MAX_CACHE_RECORDS || !descendants.insert(uuid) {
                    continue;
                }
                if let Some(node) = candidate.items.get(&uuid) {
                    pending.extend(read(&node.inner).children.keys().copied());
                }
            }
            for uuid in descendants {
                candidate.items.remove(&uuid);
            }
            if candidate.root == Some(category) {
                candidate.root = None;
            }
            if candidate.library_root == Some(category) {
                candidate.library_root = None;
            }
        }
        if candidate.items.len() > MAX_CACHE_RECORDS {
            return Err(Error::InvalidOperation);
        }
        rebuild_indexes_and_counts(&mut candidate);
        candidate.root_node = candidate
            .root
            .and_then(|uuid| candidate.items.get(&uuid).cloned());
        candidate.library_root_node = candidate
            .library_root
            .and_then(|uuid| candidate.items.get(&uuid).cloned());

        let new_values: HashMap<_, _> = candidate
            .items
            .iter()
            .filter_map(|(uuid, node)| node.value().map(|value| (*uuid, value)))
            .collect();
        *write(&self.inner.state) = candidate;

        for (uuid, old) in &old_values {
            match new_values.get(uuid) {
                None => self
                    .inner
                    .removed
                    .emit(InventoryObjectRemovedEventArgs::from_value(old.clone())),
                Some(new) if touched.contains(uuid) => {
                    self.inner
                        .updated
                        .emit(InventoryObjectUpdatedEventArgs::from_values(
                            old.clone(),
                            new.clone(),
                        ));
                }
                Some(_) => {}
            }
        }
        for (uuid, new) in new_values {
            if !old_values.contains_key(&uuid) {
                self.inner
                    .added
                    .emit(InventoryObjectAddedEventArgs::from_value(new));
            }
        }
        Ok(())
    }

    pub fn try_get_value_with_uuid_inventory_base(
        &self,
        uuid: UUID,
        item: &mut Option<Box<dyn InventoryObjectClass>>,
    ) -> Result<bool, Error> {
        *item = read(&self.inner.state)
            .items
            .get(&uuid)
            .and_then(InventoryNode::value)
            .map(|value| value.boxed());
        Ok(item.is_some())
    }
    pub fn try_get_value_with_uuid_t<T: Clone + 'static>(
        &self,
        uuid: UUID,
        item: &mut Option<T>,
    ) -> bool {
        *item = read(&self.inner.state)
            .items
            .get(&uuid)
            .and_then(InventoryNode::value)
            .and_then(|value| value.clone_as::<T>());
        item.is_some()
    }

    pub fn update_node_for(&self, item: &dyn InventoryObjectClass) -> Result<(), Error> {
        let value = InventoryValue::from_class(item).ok_or(Error::Argument)?;
        let uuid = value.base().uuid;
        let parent_uuid = value.base().parent_uuid;
        let event = {
            let mut state = write(&self.inner.state);
            // Root-level records have no hierarchy edges or ancestor counts to
            // rebuild. They are common during the initial flat inventory feed
            // and for incremental item refreshes, so update their node and link
            // index directly instead of walking and relocking the entire store.
            // The general path below remains responsible for moves, folders,
            // cycles, placeholder parents, and descendant counts.
            let old_root_value = state
                .items
                .get(&uuid)
                .and_then(InventoryNode::value)
                .filter(|old| old.base().parent_uuid == UUID::zero());
            if parent_uuid == UUID::zero()
                && (old_root_value.is_some() || !state.items.contains_key(&uuid))
            {
                if let Some(old) = old_root_value.as_ref() {
                    update_link_index(&mut state.links, uuid, old, false);
                }
                update_link_index(&mut state.links, uuid, &value, true);
                let event = if let Some(node) = state.items.get(&uuid) {
                    let old = node.value();
                    node.set_value(value.clone());
                    old.map(|old| InventoryObjectUpdatedEventArgs::from_values(old, value.clone()))
                } else {
                    state
                        .items
                        .insert(uuid, InventoryNode::from_value(value.clone()));
                    None
                };
                drop(state);
                if let Some(event) = event {
                    self.inner.updated.emit(event);
                } else {
                    self.inner
                        .added
                        .emit(InventoryObjectAddedEventArgs::from_value(value));
                }
                return Ok(());
            }
            if parent_uuid != UUID::zero() && !state.items.contains_key(&parent_uuid) {
                let mut fake = InventoryFolder::new(parent_uuid)?;
                fake.version = InventoryFolder::VERSION_UNKNOWN;
                state.items.insert(
                    parent_uuid,
                    InventoryNode::from_value(InventoryValue::Folder(fake)),
                );
            }
            let event = if let Some(node) = state.items.get(&uuid) {
                let old = node.value();
                node.set_value(value.clone());
                old.map(|old| InventoryObjectUpdatedEventArgs::from_values(old, value.clone()))
            } else {
                state
                    .items
                    .insert(uuid, InventoryNode::from_value(value.clone()));
                None
            };
            rebuild_indexes_and_counts(&mut state);
            event
        };
        if let Some(event) = event {
            self.inner.updated.emit(event);
        } else {
            self.inner
                .added
                .emit(InventoryObjectAddedEventArgs::from_value(value));
        }
        Ok(())
    }

    pub fn remove_node_for(&self, item: &dyn InventoryObjectClass) -> Result<(), Error> {
        let uuid = item.inventory_base().uuid;
        let removed = {
            let mut state = write(&self.inner.state);
            let Some(root) = state.items.get(&uuid).and_then(InventoryNode::value) else {
                return Ok(());
            };
            let mut pending = vec![uuid];
            let mut ids = HashSet::new();
            while let Some(current) = pending.pop() {
                if ids.len() >= MAX_CACHE_RECORDS || !ids.insert(current) {
                    continue;
                }
                if let Some(node) = state.items.get(&current) {
                    pending.extend(read(&node.inner).children.keys().copied());
                }
            }
            for id in ids {
                state.items.remove(&id);
            }
            if state.root == Some(uuid) {
                state.root = None;
                state.root_node = None;
            }
            if state.library_root == Some(uuid) {
                state.library_root = None;
                state.library_root_node = None;
            }
            rebuild_indexes_and_counts(&mut state);
            root
        };
        self.inner
            .removed
            .emit(InventoryObjectRemovedEventArgs::from_value(removed));
        Ok(())
    }

    pub fn count(&self) -> i32 {
        i32::try_from(read(&self.inner.state).items.len()).unwrap_or(i32::MAX)
    }
    /// Returns the item with the requested identifier.
    ///
    /// # Panics
    ///
    /// Panics when the identifier is not present, matching the indexed C#
    /// inventory accessor.
    pub fn item(&self, uuid: UUID) -> InventoryBase {
        self.get_value_or_default_with_uuid(uuid)
            .ok()
            .flatten()
            .unwrap_or_else(|| panic!("unknown inventory item: {uuid}"))
    }
    pub fn set_item(&mut self, _uuid: UUID, value: InventoryBase) {
        let _ = self.update_node_for(&value);
    }
    pub fn owner(&self) -> UUID {
        self.inner.owner
    }

    pub fn root_folder(&self) -> Option<InventoryFolder> {
        read(&self.inner.state)
            .root_node
            .as_ref()
            .and_then(InventoryNode::value)
            .and_then(|value| value.folder().cloned())
    }
    pub fn set_root_folder(&mut self, value: Option<InventoryFolder>) {
        let Some(value) = value else {
            return;
        };
        let uuid = value.base.uuid;
        if self.update_node_for(&value).is_ok() {
            let mut state = write(&self.inner.state);
            state.root = Some(uuid);
            state.root_node = state.items.get(&uuid).cloned();
        }
    }
    pub fn root_node(&self) -> InventoryNode {
        read(&self.inner.state)
            .root_node
            .clone()
            .unwrap_or_else(InventoryNode::empty)
    }
    pub fn set_root_node(&mut self, value: InventoryNode) {
        if let Some(uuid) = value.uuid() {
            let mut state = write(&self.inner.state);
            state.items.insert(uuid, value.clone());
            state.root = Some(uuid);
            state.root_node = Some(value);
            rebuild_indexes_and_counts(&mut state);
        }
    }

    pub fn library_folder(&self) -> Option<InventoryFolder> {
        read(&self.inner.state)
            .library_root_node
            .as_ref()
            .and_then(InventoryNode::value)
            .and_then(|value| value.folder().cloned())
    }
    pub fn set_library_folder(&mut self, value: Option<InventoryFolder>) {
        let Some(value) = value else {
            return;
        };
        let uuid = value.base.uuid;
        if self.update_node_for(&value).is_ok() {
            let mut state = write(&self.inner.state);
            state.library_root = Some(uuid);
            state.library_root_node = state.items.get(&uuid).cloned();
        }
    }
    pub fn library_root_node(&self) -> InventoryNode {
        read(&self.inner.state)
            .library_root_node
            .clone()
            .unwrap_or_else(InventoryNode::empty)
    }
    pub fn set_library_root_node(&mut self, value: InventoryNode) {
        if let Some(uuid) = value.uuid() {
            let mut state = write(&self.inner.state);
            state.items.insert(uuid, value.clone());
            state.library_root = Some(uuid);
            state.library_root_node = Some(value);
            rebuild_indexes_and_counts(&mut state);
        }
    }
}

fn update_link_index(
    links: &mut HashMap<UUID, HashSet<UUID>>,
    uuid: UUID,
    value: &InventoryValue,
    insert: bool,
) {
    let Some(item) = value.item() else {
        return;
    };
    if !item.is_link().unwrap_or(false) || item.asset_uuid == UUID::zero() {
        return;
    }
    if insert {
        links.entry(item.asset_uuid).or_default().insert(uuid);
    } else if let Some(records) = links.get_mut(&item.asset_uuid) {
        records.remove(&uuid);
        if records.is_empty() {
            links.remove(&item.asset_uuid);
        }
    }
}

fn rebuild_indexes_and_counts(state: &mut InventoryState) {
    state.links.clear();
    let nodes: Vec<_> = state.items.values().cloned().collect();
    for node in &nodes {
        let mut inner = write(&node.inner);
        inner.children.clear();
        inner.parent = None;
        if let Some(folder) = inner.data.as_mut().and_then(InventoryValue::folder_mut) {
            folder.descendent_count = 0;
        }
    }
    for node in &nodes {
        let Some(value) = node.value() else {
            continue;
        };
        if let Some(item) = value.item()
            && item.is_link().unwrap_or(false)
            && item.asset_uuid != UUID::zero()
        {
            state
                .links
                .entry(item.asset_uuid)
                .or_default()
                .insert(item.base.uuid);
        }
        let parent_uuid = value.base().parent_uuid;
        if parent_uuid == UUID::zero()
            || parent_uuid == value.base().uuid
            || would_create_cycle(state, value.base().uuid, parent_uuid)
        {
            continue;
        }
        if let Some(parent) = state.items.get(&parent_uuid) {
            write(&parent.inner)
                .children
                .insert(value.base().uuid, node.clone());
            write(&node.inner).parent = Some(Arc::downgrade(&parent.inner));
        }
    }
    for node in &nodes {
        let Some(value) = node.value() else {
            continue;
        };
        if value.item().is_none() {
            continue;
        }
        let mut parent = value.base().parent_uuid;
        let mut visited = HashSet::new();
        for _ in 0..MAX_HIERARCHY_DEPTH {
            if parent == UUID::zero() || !visited.insert(parent) {
                break;
            }
            let Some(parent_node) = state.items.get(&parent) else {
                break;
            };
            let next = {
                let mut inner = write(&parent_node.inner);
                let Some(parent_value) = inner.data.as_mut() else {
                    break;
                };
                if let Some(folder) = parent_value.folder_mut() {
                    folder.descendent_count = folder.descendent_count.saturating_add(1);
                }
                parent_value.base().parent_uuid
            };
            parent = next;
        }
    }
}

fn would_create_cycle(state: &InventoryState, child: UUID, mut parent: UUID) -> bool {
    let mut visited = HashSet::new();
    for _ in 0..MAX_HIERARCHY_DEPTH {
        if parent == child {
            return true;
        }
        if parent == UUID::zero() || !visited.insert(parent) {
            return false;
        }
        let Some(value) = state.items.get(&parent).and_then(InventoryNode::value) else {
            return false;
        };
        parent = value.base().parent_uuid;
    }
    true
}

fn compare_inventory(
    left: &dyn InventoryObjectClass,
    right: &dyn InventoryObjectClass,
    order: crate::InventorySortOrder,
) -> std::cmp::Ordering {
    let left_folder = left.as_any().downcast_ref::<InventoryFolder>();
    let right_folder = right.as_any().downcast_ref::<InventoryFolder>();
    let system_top = order.0 & crate::InventorySortOrder::SYSTEM_FOLDERS_TO_TOP.0 != 0;
    if system_top {
        let left_system =
            left_folder.is_some_and(|folder| folder.preferred_type != FolderType::None);
        let right_system =
            right_folder.is_some_and(|folder| folder.preferred_type != FolderType::None);
        if left_system != right_system {
            return right_system.cmp(&left_system);
        }
    }
    if left_folder.is_some() != right_folder.is_some() {
        return right_folder.is_some().cmp(&left_folder.is_some());
    }
    let by_date = order.0 & crate::InventorySortOrder::BY_DATE.0 != 0
        && !(order.0 & crate::InventorySortOrder::FOLDERS_BY_NAME.0 != 0 && left_folder.is_some());
    if by_date {
        let date = |value: &dyn InventoryObjectClass| {
            InventoryValue::from_class(value)
                .and_then(|value| value.item().map(InventoryItem::creation_date))
                .unwrap_or(UNIX_EPOCH)
        };
        date(right).cmp(&date(left)).then_with(|| {
            left.inventory_base()
                .name
                .to_lowercase()
                .cmp(&right.inventory_base().name.to_lowercase())
        })
    } else {
        left.inventory_base()
            .name
            .to_lowercase()
            .cmp(&right.inventory_base().name.to_lowercase())
    }
}

impl Inventory {
    pub fn save_to_disk_with_string(&self, filename: String) -> Result<(), Error> {
        let snapshot = self.cache_snapshot()?;
        save_cache_atomic(Path::new(&filename), &snapshot)
    }

    pub async fn save_to_disk_with_string_cancellation_token(
        &self,
        filename: String,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<(), Error> {
        check_cancelled(cancellation_token.as_ref())?;
        let snapshot = self.cache_snapshot()?;
        check_cancelled(cancellation_token.as_ref())?;
        run_blocking(move || save_cache_atomic(Path::new(&filename), &snapshot)).await?;
        check_cancelled(cancellation_token.as_ref())
    }

    pub fn restore_from_disk_with_string(&self, filename: String) -> Result<i32, Error> {
        match load_cache(Path::new(&filename)) {
            Ok(snapshot) => self.install_snapshot(snapshot),
            Err(_) => Ok(-1),
        }
    }

    pub async fn restore_from_disk_with_string_cancellation_token(
        &self,
        filename: String,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<i32, Error> {
        check_cancelled(cancellation_token.as_ref())?;
        let snapshot = run_blocking(move || load_cache(Path::new(&filename))).await?;
        check_cancelled(cancellation_token.as_ref())?;
        self.install_snapshot(snapshot)
    }

    fn cache_snapshot(&self) -> Result<CacheSnapshot, Error> {
        let state = read(&self.inner.state);
        if state.items.len() > MAX_CACHE_RECORDS {
            return Err(Error::InvalidOperation);
        }
        let mut values: Vec<_> = state
            .items
            .values()
            .filter_map(InventoryNode::value)
            .collect();
        values.sort_unstable_by_key(|value| value.base().uuid);
        Ok(CacheSnapshot {
            owner: self.inner.owner,
            root: state.root,
            library_root: state.library_root,
            values,
        })
    }

    fn install_snapshot(&self, snapshot: CacheSnapshot) -> Result<i32, Error> {
        if snapshot.owner != UUID::zero()
            && self.inner.owner != UUID::zero()
            && snapshot.owner != self.inner.owner
        {
            return Err(Error::InvalidOperation);
        }
        let restored_items = snapshot
            .values
            .iter()
            .filter(|value| value.item().is_some())
            .count();
        let mut state = InventoryState::default();
        for value in snapshot.values {
            let uuid = value.base().uuid;
            if uuid == UUID::zero()
                || state
                    .items
                    .insert(uuid, InventoryNode::from_value(value))
                    .is_some()
            {
                return Err(Error::Parse {
                    position: 0,
                    context: "duplicate or zero inventory UUID",
                });
            }
        }
        state.root = snapshot.root.filter(|uuid| state.items.contains_key(uuid));
        state.library_root = snapshot
            .library_root
            .filter(|uuid| state.items.contains_key(uuid));
        state.root_node = state.root.and_then(|uuid| state.items.get(&uuid).cloned());
        state.library_root_node = state
            .library_root
            .and_then(|uuid| state.items.get(&uuid).cloned());
        rebuild_indexes_and_counts(&mut state);
        *write(&self.inner.state) = state;
        i32::try_from(restored_items).map_err(|_| Error::InvalidOperation)
    }
}

struct CacheSnapshot {
    owner: UUID,
    root: Option<UUID>,
    library_root: Option<UUID>,
    values: Vec<InventoryValue>,
}

async fn run_blocking<T, F>(operation: F) -> Result<T, Error>
where
    T: Send + 'static,
    F: FnOnce() -> Result<T, Error> + Send + 'static,
{
    let (sender, receiver) = futures_channel::oneshot::channel();
    std::thread::Builder::new()
        .name("libremetaverse-inventory-io".into())
        .spawn(move || {
            let _ = sender.send(operation());
        })
        .map_err(|_| Error::InvalidOperation)?;
    receiver.await.map_err(|_| Error::InvalidOperation)?
}

fn check_cancelled(token: Option<&CancellationToken>) -> Result<(), Error> {
    token.map_or(Ok(()), CancellationToken::throw_if_cancellation_requested)
}

fn save_cache_atomic(path: &Path, snapshot: &CacheSnapshot) -> Result<(), Error> {
    let bytes = encode_cache(snapshot)?;
    if bytes.len() as u64 > MAX_CACHE_BYTES {
        return Err(Error::InvalidOperation);
    }
    let parent = path
        .parent()
        .filter(|parent| !parent.as_os_str().is_empty())
        .unwrap_or_else(|| Path::new("."));
    let name = path
        .file_name()
        .and_then(|name| name.to_str())
        .ok_or(Error::Argument)?;
    let nonce = UUID::random()?.to_string();
    let temporary = parent.join(format!(".{name}.{nonce}.tmp"));
    let result = (|| {
        let mut file = OpenOptions::new()
            .create_new(true)
            .write(true)
            .open(&temporary)
            .map_err(|_| Error::InvalidOperation)?;
        file.write_all(&bytes)
            .map_err(|_| Error::InvalidOperation)?;
        file.sync_all().map_err(|_| Error::InvalidOperation)?;
        drop(file);
        replace_file(&temporary, path)?;
        if let Ok(directory) = File::open(parent) {
            let _ = directory.sync_all();
        }
        Ok(())
    })();
    if result.is_err() {
        let _ = fs::remove_file(&temporary);
    }
    result
}

fn replace_file(temporary: &Path, destination: &Path) -> Result<(), Error> {
    match fs::rename(temporary, destination) {
        Ok(()) => Ok(()),
        Err(_) if destination.exists() => {
            let backup = backup_path(destination)?;
            fs::rename(destination, &backup).map_err(|_| Error::InvalidOperation)?;
            if fs::rename(temporary, destination).is_ok() {
                let _ = fs::remove_file(backup);
                Ok(())
            } else {
                let _ = fs::rename(backup, destination);
                Err(Error::InvalidOperation)
            }
        }
        Err(_) => Err(Error::InvalidOperation),
    }
}

fn backup_path(destination: &Path) -> Result<PathBuf, Error> {
    let parent = destination.parent().unwrap_or_else(|| Path::new("."));
    let name = destination
        .file_name()
        .and_then(|name| name.to_str())
        .ok_or(Error::Argument)?;
    Ok(parent.join(format!(".{name}.{}.bak", UUID::random()?.to_string())))
}

fn load_cache(path: &Path) -> Result<CacheSnapshot, Error> {
    let metadata = fs::metadata(path).map_err(|_| Error::InvalidOperation)?;
    if metadata.len() > MAX_CACHE_BYTES {
        return Err(Error::InvalidOperation);
    }
    let capacity = usize::try_from(metadata.len()).map_err(|_| Error::InvalidOperation)?;
    let mut bytes = Vec::with_capacity(capacity);
    File::open(path)
        .map(|file| file.take(MAX_CACHE_BYTES + 1))
        .and_then(|mut file| file.read_to_end(&mut bytes))
        .map_err(|_| Error::InvalidOperation)?;
    if bytes.len() as u64 > MAX_CACHE_BYTES {
        return Err(Error::InvalidOperation);
    }
    decode_cache(&bytes)
}

fn encode_cache(snapshot: &CacheSnapshot) -> Result<Vec<u8>, Error> {
    let mut output = Vec::new();
    cache_extend(&mut output, CACHE_MAGIC)?;
    cache_extend(&mut output, &CACHE_VERSION.to_le_bytes())?;
    write_uuid(&mut output, snapshot.owner)?;
    write_optional_uuid(&mut output, snapshot.root)?;
    write_optional_uuid(&mut output, snapshot.library_root)?;
    cache_extend(
        &mut output,
        &u32::try_from(snapshot.values.len())
            .map_err(|_| Error::InvalidOperation)?
            .to_le_bytes(),
    )?;
    for value in &snapshot.values {
        encode_value(&mut output, value)?;
    }
    Ok(output)
}

fn encode_value(output: &mut Vec<u8>, value: &InventoryValue) -> Result<(), Error> {
    cache_push(output, value.tag())?;
    let base = value.base();
    write_uuid(output, base.uuid)?;
    write_uuid(output, base.parent_uuid)?;
    write_uuid(output, base.owner_id)?;
    write_string(output, &base.name)?;
    if let Some(folder) = value.folder() {
        cache_push(output, (folder.preferred_type as i8).cast_unsigned())?;
        cache_extend(output, &folder.version.to_le_bytes())?;
        cache_extend(output, &folder.descendent_count.to_le_bytes())?;
    } else if let Some(item) = value.item() {
        write_uuid(output, item.asset_uuid)?;
        for mask in [
            item.permissions.base_mask,
            item.permissions.everyone_mask,
            item.permissions.group_mask,
            item.permissions.next_owner_mask,
            item.permissions.owner_mask,
        ] {
            cache_extend(output, &mask.0.to_le_bytes())?;
        }
        cache_push(output, (item.asset_type as i8).cast_unsigned())?;
        cache_push(output, item.inventory_type.0.cast_unsigned())?;
        write_uuid(output, item.creator_id)?;
        write_string(output, &item.description)?;
        write_uuid(output, item.group_id)?;
        cache_push(output, u8::from(item.group_owned))?;
        cache_extend(output, &item.sale_price.to_le_bytes())?;
        cache_push(output, item.sale_type as u8)?;
        cache_extend(output, &item.flags.to_le_bytes())?;
        let (seconds, nanos) = system_time_parts(item.creation_date)?;
        cache_extend(output, &seconds.to_le_bytes())?;
        cache_extend(output, &nanos.to_le_bytes())?;
        write_uuid(output, item.transaction_id)?;
        write_uuid(output, item.last_owner_id)?;
    }
    Ok(())
}

fn decode_cache(bytes: &[u8]) -> Result<CacheSnapshot, Error> {
    let mut input = CacheReader { bytes, position: 0 };
    if input.take(CACHE_MAGIC.len())? != CACHE_MAGIC {
        return input.error("invalid inventory cache magic");
    }
    if input.u32()? != CACHE_VERSION {
        return input.error("unsupported inventory cache version");
    }
    let owner = input.uuid()?;
    let root = input.optional_uuid()?;
    let library_root = input.optional_uuid()?;
    let count = usize::try_from(input.u32()?).map_err(|_| Error::InvalidOperation)?;
    if count > MAX_CACHE_RECORDS {
        return input.error("inventory cache record limit exceeded");
    }
    let mut values = Vec::with_capacity(count);
    for _ in 0..count {
        values.push(decode_value(&mut input)?);
    }
    if input.position != bytes.len() {
        return input.error("trailing inventory cache bytes");
    }
    Ok(CacheSnapshot {
        owner,
        root,
        library_root,
        values,
    })
}

fn decode_value(input: &mut CacheReader<'_>) -> Result<InventoryValue, Error> {
    let tag = input.u8()?;
    let base = InventoryBase {
        uuid: input.uuid()?,
        parent_uuid: input.uuid()?,
        owner_id: input.uuid()?,
        name: input.string()?,
    };
    if tag == 0 {
        return Ok(InventoryValue::Base(base));
    }
    if tag == 1 {
        return Ok(InventoryValue::Folder(InventoryFolder {
            base,
            preferred_type: folder_type_from_i32(i32::from(input.i8()?))?,
            version: input.i32()?,
            descendent_count: input.i32()?,
        }));
    }
    let item = InventoryItem {
        base,
        asset_uuid: input.uuid()?,
        permissions: Permissions {
            base_mask: PermissionMask(input.u32()?),
            everyone_mask: PermissionMask(input.u32()?),
            group_mask: PermissionMask(input.u32()?),
            next_owner_mask: PermissionMask(input.u32()?),
            owner_mask: PermissionMask(input.u32()?),
        },
        asset_type: asset_type_from_i32(i32::from(input.i8()?))?,
        inventory_type: InventoryType(input.i8()?),
        creator_id: input.uuid()?,
        description: input.string()?,
        group_id: input.uuid()?,
        group_owned: input.u8()? != 0,
        sale_price: input.i32()?,
        sale_type: sale_type_from_i32(i32::from(input.u8()?))?,
        flags: input.u32()?,
        creation_date: system_time_from_parts(input.i64()?, input.u32()?)?,
        transaction_id: input.uuid()?,
        last_owner_id: input.uuid()?,
    };
    Ok(match tag {
        2 => InventoryValue::Item(item),
        3 => InventoryValue::Animation(InventoryAnimation { base: item }),
        4 => InventoryValue::Attachment(InventoryAttachment { base: item }),
        5 => InventoryValue::CallingCard(InventoryCallingCard { base: item }),
        6 => InventoryValue::Category(InventoryCategory { base: item }),
        7 => InventoryValue::Gesture(InventoryGesture { base: item }),
        8 => InventoryValue::Lsl(InventoryLSL { base: item }),
        9 => InventoryValue::Landmark(InventoryLandmark { base: item }),
        10 => InventoryValue::Material(InventoryMaterial { base: item }),
        11 => InventoryValue::Notecard(InventoryNotecard { base: item }),
        12 => InventoryValue::Object(InventoryObject { base: item }),
        13 => InventoryValue::Settings(InventorySettings { base: item }),
        14 => InventoryValue::Snapshot(InventorySnapshot { base: item }),
        15 => InventoryValue::Sound(InventorySound { base: item }),
        16 => InventoryValue::Texture(InventoryTexture { base: item }),
        17 => InventoryValue::Wearable(InventoryWearable { base: item }),
        _ => return input.error("unknown inventory cache record tag"),
    })
}

struct CacheReader<'a> {
    bytes: &'a [u8],
    position: usize,
}
impl<'a> CacheReader<'a> {
    fn error<T>(&self, context: &'static str) -> Result<T, Error> {
        Err(Error::Parse {
            position: self.position,
            context,
        })
    }
    fn take(&mut self, count: usize) -> Result<&'a [u8], Error> {
        let end = self
            .position
            .checked_add(count)
            .ok_or(Error::InvalidOperation)?;
        let result = self.bytes.get(self.position..end).ok_or(Error::Parse {
            position: self.position,
            context: "truncated inventory cache",
        })?;
        self.position = end;
        Ok(result)
    }
    fn u8(&mut self) -> Result<u8, Error> {
        Ok(self.take(1)?[0])
    }
    fn i8(&mut self) -> Result<i8, Error> {
        Ok(self.u8()?.cast_signed())
    }
    fn u32(&mut self) -> Result<u32, Error> {
        Ok(u32::from_le_bytes(
            self.take(4)?
                .try_into()
                .map_err(|_| Error::InvalidOperation)?,
        ))
    }
    fn i32(&mut self) -> Result<i32, Error> {
        Ok(i32::from_le_bytes(
            self.take(4)?
                .try_into()
                .map_err(|_| Error::InvalidOperation)?,
        ))
    }
    fn i64(&mut self) -> Result<i64, Error> {
        Ok(i64::from_le_bytes(
            self.take(8)?
                .try_into()
                .map_err(|_| Error::InvalidOperation)?,
        ))
    }
    fn uuid(&mut self) -> Result<UUID, Error> {
        UUID::new_with_bytes_int32(self.take(16)?.to_vec(), 0)
    }
    fn optional_uuid(&mut self) -> Result<Option<UUID>, Error> {
        Ok(if self.u8()? == 0 {
            None
        } else {
            Some(self.uuid()?)
        })
    }
    fn string(&mut self) -> Result<String, Error> {
        let count = usize::try_from(self.u32()?).map_err(|_| Error::InvalidOperation)?;
        if count > MAX_STRING_BYTES {
            return self.error("inventory cache string limit exceeded");
        }
        String::from_utf8(self.take(count)?.to_vec()).map_err(|_| Error::Parse {
            position: self.position,
            context: "invalid inventory cache UTF-8",
        })
    }
}

fn write_uuid(output: &mut Vec<u8>, value: UUID) -> Result<(), Error> {
    cache_extend(output, &value.get_bytes()?)
}
fn write_optional_uuid(output: &mut Vec<u8>, value: Option<UUID>) -> Result<(), Error> {
    cache_push(output, u8::from(value.is_some()))?;
    if let Some(value) = value {
        write_uuid(output, value)?;
    }
    Ok(())
}
fn write_string(output: &mut Vec<u8>, value: &str) -> Result<(), Error> {
    if value.len() > MAX_STRING_BYTES {
        return Err(Error::InvalidOperation);
    }
    cache_extend(
        output,
        &u32::try_from(value.len())
            .map_err(|_| Error::InvalidOperation)?
            .to_le_bytes(),
    )?;
    cache_extend(output, value.as_bytes())
}

fn cache_push(output: &mut Vec<u8>, value: u8) -> Result<(), Error> {
    cache_extend(output, &[value])
}

fn cache_extend(output: &mut Vec<u8>, value: &[u8]) -> Result<(), Error> {
    let length = output
        .len()
        .checked_add(value.len())
        .ok_or(Error::InvalidOperation)?;
    if length > usize::try_from(MAX_CACHE_BYTES).map_err(|_| Error::InvalidOperation)? {
        return Err(Error::InvalidOperation);
    }
    output.extend_from_slice(value);
    Ok(())
}

fn system_time_parts(value: SystemTime) -> Result<(i64, u32), Error> {
    match value.duration_since(UNIX_EPOCH) {
        Ok(duration) => Ok((
            i64::try_from(duration.as_secs()).map_err(|_| Error::InvalidOperation)?,
            duration.subsec_nanos(),
        )),
        Err(error) => {
            let duration = error.duration();
            let seconds = i64::try_from(duration.as_secs()).map_err(|_| Error::InvalidOperation)?;
            if duration.subsec_nanos() == 0 {
                Ok((-seconds, 0))
            } else {
                Ok((
                    seconds
                        .checked_neg()
                        .and_then(|value| value.checked_sub(1))
                        .ok_or(Error::InvalidOperation)?,
                    1_000_000_000 - duration.subsec_nanos(),
                ))
            }
        }
    }
}
fn system_time_from_parts(seconds: i64, nanos: u32) -> Result<SystemTime, Error> {
    if nanos >= 1_000_000_000 {
        return Err(Error::Argument);
    }
    if seconds >= 0 {
        Ok(UNIX_EPOCH + Duration::new(u64::try_from(seconds).map_err(|_| Error::Argument)?, nanos))
    } else {
        UNIX_EPOCH
            .checked_sub(Duration::from_secs(seconds.unsigned_abs()))
            .and_then(|value| value.checked_add(Duration::from_nanos(u64::from(nanos))))
            .ok_or(Error::Argument)
    }
}

fn osd_i32(map: &HashMap<String, OSD>, key: &str) -> Result<i32, Error> {
    map.get(key).map_or(Ok(0), OSD::as_integer)
}
fn osd_u32(map: &HashMap<String, OSD>, key: &str) -> Result<u32, Error> {
    map.get(key).map_or(Ok(0), OSD::as_u_integer)
}
fn osd_uuid(map: &HashMap<String, OSD>, key: &str) -> Result<UUID, Error> {
    map.get(key).ok_or(Error::Argument)?.as_uuid()
}
fn optional_uuid(map: &HashMap<String, OSD>, key: &str) -> Result<Option<UUID>, Error> {
    map.get(key).map(OSD::as_uuid).transpose()
}

fn parse_asset_type(value: Option<&OSD>) -> Result<AssetType, Error> {
    match value {
        None => Ok(AssetType::Unknown),
        Some(OSD::String(value)) => Ok(match value.to_ascii_lowercase().as_str() {
            "texture" => AssetType::Texture,
            "sound" => AssetType::Sound,
            "callcard" | "callingcard" => AssetType::CallingCard,
            "landmark" => AssetType::Landmark,
            "script" => AssetType::Script,
            "clothing" => AssetType::Clothing,
            "object" => AssetType::Object,
            "notecard" => AssetType::Notecard,
            "category" | "folder" => AssetType::Folder,
            "lsltext" => AssetType::LSLText,
            "lslbyte" | "lslbytecode" => AssetType::LSLBytecode,
            "txtr_tga" => AssetType::TextureTGA,
            "bodypart" => AssetType::Bodypart,
            "snd_wav" => AssetType::SoundWAV,
            "img_tga" => AssetType::ImageTGA,
            "jpeg" | "img_jpeg" => AssetType::ImageJPEG,
            "animatn" | "animation" => AssetType::Animation,
            "gesture" => AssetType::Gesture,
            "simstate" => AssetType::Simstate,
            "link" => AssetType::Link,
            "link_f" | "linkfolder" => AssetType::LinkFolder,
            "mesh" => AssetType::Mesh,
            "settings" => AssetType::Settings,
            "material" => AssetType::Material,
            _ => AssetType::Unknown,
        }),
        Some(value) => asset_type_from_i32(value.as_integer()?),
    }
}

fn parse_inventory_type(value: Option<&OSD>) -> Result<InventoryType, Error> {
    match value {
        None => Ok(InventoryType::UNKNOWN),
        Some(OSD::String(value)) => Ok(match value.to_ascii_lowercase().as_str() {
            "texture" => InventoryType::TEXTURE,
            "sound" => InventoryType::SOUND,
            "callcard" | "callingcard" => InventoryType::CALLING_CARD,
            "landmark" => InventoryType::LANDMARK,
            "object" => InventoryType::OBJECT,
            "notecard" => InventoryType::NOTECARD,
            "category" | "folder" => InventoryType::CATEGORY,
            "root" | "rootcategory" => InventoryType::ROOT_CATEGORY,
            "lsl" | "script" => InventoryType::LSL,
            "snapshot" => InventoryType::SNAPSHOT,
            "attach" | "attachment" => InventoryType::ATTACHMENT,
            "wearable" => InventoryType::WEARABLE,
            "animation" => InventoryType::ANIMATION,
            "gesture" => InventoryType::GESTURE,
            "mesh" => InventoryType::MESH,
            "settings" => InventoryType::SETTINGS,
            "material" => InventoryType::MATERIAL,
            _ => InventoryType::UNKNOWN,
        }),
        Some(value) => Ok(InventoryType(
            i8::try_from(value.as_integer()?).map_err(|_| Error::Argument)?,
        )),
    }
}

fn inventory_type_name(value: InventoryType) -> String {
    match value {
        InventoryType::UNKNOWN => "Unknown".into(),
        InventoryType::TEXTURE => "Texture".into(),
        InventoryType::SOUND => "Sound".into(),
        InventoryType::CALLING_CARD => "CallingCard".into(),
        InventoryType::LANDMARK => "Landmark".into(),
        InventoryType::OBJECT => "Object".into(),
        InventoryType::NOTECARD => "Notecard".into(),
        InventoryType::CATEGORY => "Category".into(),
        InventoryType::ROOT_CATEGORY => "RootCategory".into(),
        InventoryType::LSL => "LSL".into(),
        InventoryType::SNAPSHOT => "Snapshot".into(),
        InventoryType::ATTACHMENT => "Attachment".into(),
        InventoryType::WEARABLE => "Wearable".into(),
        InventoryType::ANIMATION => "Animation".into(),
        InventoryType::GESTURE => "Gesture".into(),
        InventoryType::MESH => "Mesh".into(),
        InventoryType::SETTINGS => "Settings".into(),
        InventoryType::MATERIAL => "Material".into(),
        InventoryType(other) => other.to_string(),
    }
}

fn decrypt_shadow_id(shadow_id: UUID) -> Result<UUID, Error> {
    let magic = UUID::new_with_string("3c115e51-04f4-523c-9fa6-98aff1034730".into())?;
    Ok(UUID::bitxor(shadow_id, magic))
}

fn asset_type_from_i32(value: i32) -> Result<AssetType, Error> {
    Ok(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,
        _ => return Err(Error::Argument),
    })
}

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

fn sale_type_from_i32(value: i32) -> Result<SaleType, Error> {
    Ok(match value {
        0 => SaleType::Not,
        1 => SaleType::Original,
        2 => SaleType::Copy,
        3 => SaleType::Contents,
        _ => return Err(Error::Argument),
    })
}

const fn attachment_point_from_u8(value: u8) -> AttachmentPoint {
    match value {
        1 => AttachmentPoint::Chest,
        2 => AttachmentPoint::Skull,
        3 => AttachmentPoint::LeftShoulder,
        4 => AttachmentPoint::RightShoulder,
        5 => AttachmentPoint::LeftHand,
        6 => AttachmentPoint::RightHand,
        7 => AttachmentPoint::LeftFoot,
        8 => AttachmentPoint::RightFoot,
        9 => AttachmentPoint::Spine,
        10 => AttachmentPoint::Pelvis,
        11 => AttachmentPoint::Mouth,
        12 => AttachmentPoint::Chin,
        13 => AttachmentPoint::LeftEar,
        14 => AttachmentPoint::RightEar,
        15 => AttachmentPoint::LeftEyeball,
        16 => AttachmentPoint::RightEyeball,
        17 => AttachmentPoint::Nose,
        18 => AttachmentPoint::RightUpperArm,
        19 => AttachmentPoint::RightForearm,
        20 => AttachmentPoint::LeftUpperArm,
        21 => AttachmentPoint::LeftForearm,
        22 => AttachmentPoint::RightHip,
        23 => AttachmentPoint::RightUpperLeg,
        24 => AttachmentPoint::RightLowerLeg,
        25 => AttachmentPoint::LeftHip,
        26 => AttachmentPoint::LeftUpperLeg,
        27 => AttachmentPoint::LeftLowerLeg,
        28 => AttachmentPoint::Stomach,
        29 => AttachmentPoint::LeftPec,
        30 => AttachmentPoint::RightPec,
        31 => AttachmentPoint::HUDCenter2,
        32 => AttachmentPoint::HUDTopRight,
        33 => AttachmentPoint::HUDTop,
        34 => AttachmentPoint::HUDTopLeft,
        35 => AttachmentPoint::HUDCenter,
        36 => AttachmentPoint::HUDBottomLeft,
        37 => AttachmentPoint::HUDBottom,
        38 => AttachmentPoint::HUDBottomRight,
        39 => AttachmentPoint::Neck,
        40 => AttachmentPoint::Root,
        41 => AttachmentPoint::LeftHandRing,
        42 => AttachmentPoint::RightHandRing,
        43 => AttachmentPoint::TailBase,
        44 => AttachmentPoint::TailTip,
        45 => AttachmentPoint::LeftWing,
        46 => AttachmentPoint::RightWing,
        47 => AttachmentPoint::Jaw,
        48 => AttachmentPoint::AltLeftEar,
        49 => AttachmentPoint::AltRightEar,
        50 => AttachmentPoint::AltLeftEye,
        51 => AttachmentPoint::AltRightEye,
        52 => AttachmentPoint::Tongue,
        53 => AttachmentPoint::Groin,
        54 => AttachmentPoint::LeftHindFoot,
        55 => AttachmentPoint::RightHindFoot,
        _ => AttachmentPoint::Default,
    }
}

const fn wearable_type_from_u8(value: u8) -> WearableType {
    match value {
        0 => WearableType::Shape,
        1 => WearableType::Skin,
        2 => WearableType::Hair,
        3 => WearableType::Eyes,
        4 => WearableType::Shirt,
        5 => WearableType::Pants,
        6 => WearableType::Shoes,
        7 => WearableType::Socks,
        8 => WearableType::Jacket,
        9 => WearableType::Gloves,
        10 => WearableType::Undershirt,
        11 => WearableType::Underpants,
        12 => WearableType::Skirt,
        13 => WearableType::Alpha,
        14 => WearableType::Tattoo,
        15 => WearableType::Physics,
        16 => WearableType::Universal,
        _ => WearableType::Invalid,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::atomic::{AtomicUsize, Ordering};

    fn fixture() -> (Arc<GridClient>, Inventory) {
        let client = Arc::new(GridClient::new().expect("client"));
        let inventory =
            Inventory::new_with_grid_client_uuid(Arc::clone(&client), UUID::random().unwrap())
                .unwrap();
        (client, inventory)
    }

    fn temporary_file(name: &str) -> PathBuf {
        std::env::temp_dir().join(format!(
            "metacrate-{name}-{}",
            UUID::random().unwrap().to_string()
        ))
    }

    #[test]
    fn cache_round_trip_preserves_roots_concrete_type_and_permissions() {
        let (_client, mut inventory) = fixture();
        let mut root = InventoryFolder::new(UUID::random().unwrap()).unwrap();
        root.base.set_name("My Inventory".into());
        root.set_preferred_type(FolderType::Root);
        let root_id = root.base.uuid();
        inventory.set_root_folder(Some(root));

        let mut library = InventoryFolder::new(UUID::random().unwrap()).unwrap();
        library.base.set_name("Library".into());
        library.set_preferred_type(FolderType::BasicRoot);
        let library_id = library.base.uuid();
        inventory.set_library_folder(Some(library));

        let mut wearable = InventoryWearable::new(UUID::random().unwrap()).unwrap();
        wearable.base.base.set_parent_uuid(root_id);
        wearable.base.base.set_name("Shirt".into());
        wearable.base.set_asset_type(AssetType::Clothing);
        wearable.base.set_asset_uuid(UUID::random().unwrap());
        wearable
            .base
            .set_permissions(Permissions::new(1, 2, 3, 4, 5).unwrap());
        wearable
            .base
            .set_creation_date(UNIX_EPOCH - Duration::from_millis(500));
        wearable.set_wearable_type(WearableType::Shirt);
        let wearable_id = wearable.base.base.uuid();
        inventory.update_node_for(&wearable).unwrap();

        let path = temporary_file("inventory-roundtrip");
        inventory
            .save_to_disk_with_string(path.to_string_lossy().into_owned())
            .unwrap();
        inventory.clear().unwrap();
        assert_eq!(inventory.count(), 0);
        assert_eq!(inventory.root_folder().unwrap().base.uuid(), root_id);
        assert_eq!(inventory.library_folder().unwrap().base.uuid(), library_id);
        assert_eq!(
            inventory
                .restore_from_disk_with_string(path.to_string_lossy().into_owned())
                .unwrap(),
            1
        );
        let restored = inventory
            .get_value_or_default_with_uuid_a7c63fbe::<InventoryWearable>(wearable_id)
            .unwrap()
            .unwrap();
        assert_eq!(restored.wearable_type(), WearableType::Shirt);
        assert_eq!(
            restored.base.creation_date(),
            UNIX_EPOCH - Duration::from_millis(500)
        );
        assert_eq!(
            restored.base.permissions(),
            Permissions::new(1, 2, 3, 4, 5).unwrap()
        );
        assert_eq!(inventory.root_folder().unwrap().base.uuid(), root_id);
        assert_eq!(inventory.library_folder().unwrap().base.uuid(), library_id);
        let _ = fs::remove_file(path);
    }

    #[test]
    fn models_preserve_csharp_equality_shadow_and_indexer_rules() {
        let item_id = UUID::random().unwrap();
        let mut left = InventoryItem::new_with_uuid(item_id).unwrap();
        let mut right = left.clone();
        left.set_creator_id(UUID::random().unwrap());
        right.set_creator_id(UUID::random().unwrap());
        left.set_transaction_id(UUID::random().unwrap());
        right.set_transaction_id(UUID::random().unwrap());
        assert_eq!(left, right, "C# equality excludes transient IDs");

        let asset_id = UUID::random().unwrap();
        let shadow_id = UUID::bitxor(
            asset_id,
            UUID::new_with_string("3c115e51-04f4-523c-9fa6-98aff1034730".into()).unwrap(),
        );
        left.update(
            OSDMap::new_with_dictionary(HashMap::from([(
                "shadow_id".into(),
                OSD::UUID(shadow_id),
            )]))
            .unwrap(),
        )
        .unwrap();
        assert_eq!(left.asset_uuid(), asset_id);

        let (_client, mut inventory) = fixture();
        let folder = InventoryFolder::new(UUID::random().unwrap()).unwrap();
        let folder_id = folder.base.uuid();
        let mismatched_key = UUID::random().unwrap();
        inventory.set_item(mismatched_key, folder.base);
        assert!(inventory.contains_with_uuid(folder_id).unwrap());
        assert!(!inventory.contains_with_uuid(mismatched_key).unwrap());
    }

    #[tokio::test]
    async fn cache_rejects_corruption_and_unknown_versions() {
        let (_client, inventory) = fixture();
        let path = temporary_file("inventory-corrupt");
        fs::write(&path, b"not-an-inventory-cache").unwrap();
        assert_eq!(
            inventory
                .restore_from_disk_with_string(path.to_string_lossy().into_owned())
                .unwrap(),
            -1
        );
        assert!(
            inventory
                .restore_from_disk_with_string_cancellation_token(
                    path.to_string_lossy().into_owned(),
                    None
                )
                .await
                .is_err()
        );

        let snapshot = inventory.cache_snapshot().unwrap();
        let mut bytes = encode_cache(&snapshot).unwrap();
        bytes[8..12].copy_from_slice(&99_u32.to_le_bytes());
        fs::write(&path, bytes).unwrap();
        assert_eq!(
            inventory
                .restore_from_disk_with_string(path.to_string_lossy().into_owned())
                .unwrap(),
            -1
        );
        assert!(
            inventory
                .restore_from_disk_with_string_cancellation_token(
                    path.to_string_lossy().into_owned(),
                    None
                )
                .await
                .is_err()
        );
        let _ = fs::remove_file(path);
    }

    #[test]
    fn callbacks_run_after_store_lock_is_released() {
        let (_client, inventory) = fixture();
        let reentrant = inventory.clone();
        let calls = Arc::new(AtomicUsize::new(0));
        let observed = Arc::clone(&calls);
        let _subscription = inventory.subscribe_inventory_object_added(Arc::new(move |event| {
            assert!(reentrant.contains_with_uuid(event.obj().uuid()).unwrap());
            assert!(reentrant.count() > 0);
            observed.fetch_add(1, Ordering::SeqCst);
        }));
        let folder = InventoryFolder::new(UUID::random().unwrap()).unwrap();
        inventory.update_node_for(&folder).unwrap();
        assert_eq!(calls.load(Ordering::SeqCst), 1);
    }

    #[test]
    fn root_item_fast_path_keeps_link_index_and_events_consistent() {
        let (_client, inventory) = fixture();
        let item_id = UUID::random().unwrap();
        let first_target = UUID::random().unwrap();
        let second_target = UUID::random().unwrap();
        let mut item = InventoryItem::new_with_uuid(item_id).unwrap();
        item.set_asset_type(AssetType::Link);
        item.set_asset_uuid(first_target);
        inventory.update_node_for(&item).unwrap();
        assert_eq!(inventory.find_all_links(first_target).unwrap().len(), 1);

        item.set_asset_uuid(second_target);
        inventory.update_node_for(&item).unwrap();
        assert!(inventory.find_all_links(first_target).unwrap().is_empty());
        assert_eq!(inventory.find_all_links(second_target).unwrap().len(), 1);

        item.set_asset_type(AssetType::Texture);
        inventory.update_node_for(&item).unwrap();
        assert!(inventory.find_all_links(second_target).unwrap().is_empty());
        assert_eq!(inventory.count(), 1);
    }

    #[test]
    fn system_folders_sort_first_and_are_discoverable() {
        let (_client, mut inventory) = fixture();
        let root = InventoryFolder::new(UUID::random().unwrap()).unwrap();
        let root_id = root.base.uuid();
        inventory.set_root_folder(Some(root));
        let mut ordinary = InventoryFolder::new(UUID::random().unwrap()).unwrap();
        ordinary.base.set_parent_uuid(root_id);
        ordinary.base.set_name("A ordinary".into());
        inventory.update_node_for(&ordinary).unwrap();
        let mut trash = InventoryFolder::new(UUID::random().unwrap()).unwrap();
        trash.base.set_parent_uuid(root_id);
        trash.base.set_name("Z trash".into());
        trash.set_preferred_type(FolderType::Trash);
        let trash_id = trash.base.uuid();
        inventory.update_node_for(&trash).unwrap();
        let sorted = inventory
            .get_contents_sorted_with_uuid(
                root_id,
                crate::InventorySortOrder(crate::InventorySortOrder::SYSTEM_FOLDERS_TO_TOP.0),
            )
            .unwrap();
        assert_eq!(sorted[0].inventory_base().uuid(), trash_id);
        assert_eq!(
            inventory.find_folder_for_type(FolderType::Trash),
            Some(trash_id)
        );
    }
}
