//! Current Outfit Folder state, policy composition, and transactional mutations.

use crate::appearance::ICurrentOutfitPolicy;
use crate::client_core::ClientWeakHandle;
use crate::{
    GridClient, Inventory, InventoryBase, InventoryFolder, InventoryItem, InventoryObject,
    InventoryObjectClass, InventoryWearable, Primitive,
};
use libremetaverse_types::compat::{CancellationToken, Object};
use libremetaverse_types::{AssetType, AttachmentPoint, Error, FolderType, UUID, WearableType};
use std::collections::{HashMap, HashSet};
use std::future::Future;
use std::pin::Pin;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, RwLock};

pub(crate) const MAX_CLOTHING_LAYERS: i32 = 60;
const MAX_ANCESTRY_DEPTH: usize = 256;

type BackendFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T, Error>> + 'a>>;

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

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

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

pub(crate) fn default_policy_decision(item: Option<Arc<InventoryItem>>) -> Result<bool, Error> {
    item.ok_or(Error::ArgumentNull).map(|_| true)
}

pub(crate) fn default_policy_report(
    added: Option<Arc<Vec<InventoryItem>>>,
    removed: Option<Arc<Vec<InventoryItem>>>,
    cancellation_token: Option<CancellationToken>,
) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send>> {
    Box::pin(async move {
        let _ = (added, removed);
        check_cancelled(cancellation_token.as_ref())
    })
}

/// Thread-safe AND-composition of independent, pure outfit policies.
pub struct CompositeCurrentOutfitPolicy {
    policies: RwLock<Vec<Arc<dyn ICurrentOutfitPolicy>>>,
}

impl CompositeCurrentOutfitPolicy {
    #[allow(clippy::unnecessary_wraps)] // The mapped constructor is fallible.
    pub(crate) fn native_new() -> Result<Self, Error> {
        Ok(Self {
            policies: RwLock::new(Vec::new()),
        })
    }

    pub(crate) fn native_add_policy(
        &self,
        policy: Option<Arc<dyn ICurrentOutfitPolicy>>,
    ) -> Result<&Self, Error> {
        let policy = policy.ok_or(Error::ArgumentNull)?;
        let mut policies = write(&self.policies);
        if !policies
            .iter()
            .any(|candidate| Arc::ptr_eq(candidate, &policy))
        {
            policies.push(policy);
        }
        Ok(self)
    }

    #[allow(clippy::unnecessary_wraps)] // The mapped removal method is fallible.
    pub(crate) fn native_remove_policy(
        &self,
        policy: Option<Arc<dyn ICurrentOutfitPolicy>>,
    ) -> Result<(), Error> {
        let Some(policy) = policy else {
            return Ok(());
        };
        write(&self.policies).retain(|candidate| !Arc::ptr_eq(candidate, &policy));
        Ok(())
    }

    fn snapshot(&self) -> Vec<Arc<dyn ICurrentOutfitPolicy>> {
        read(&self.policies).clone()
    }

    pub(crate) fn native_can_attach(
        &self,
        item: Option<Arc<InventoryItem>>,
    ) -> Result<bool, Error> {
        let item = item.ok_or(Error::ArgumentNull)?;
        let policies = self.snapshot();
        let mut allowed = true;
        for policy in policies {
            // Consult every policy even after a denial. Policies may record an
            // audit decision, and the C# composite preserves that behavior.
            allowed &= policy.can_attach(Some(Arc::clone(&item)))?;
        }
        Ok(allowed)
    }

    pub(crate) fn native_can_detach(
        &self,
        item: Option<Arc<InventoryItem>>,
    ) -> Result<bool, Error> {
        let item = item.ok_or(Error::ArgumentNull)?;
        let policies = self.snapshot();
        let mut allowed = true;
        for policy in policies {
            allowed &= policy.can_detach(Some(Arc::clone(&item)))?;
        }
        Ok(allowed)
    }

    pub(crate) async fn native_report_item_change(
        &self,
        added: Option<Arc<Vec<InventoryItem>>>,
        removed: Option<Arc<Vec<InventoryItem>>>,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<(), Error> {
        check_cancelled(cancellation_token.as_ref())?;
        let policies = self.snapshot();
        for policy in policies {
            check_cancelled(cancellation_token.as_ref())?;
            policy
                .report_item_change(
                    added.as_ref().map(Arc::clone),
                    removed.as_ref().map(Arc::clone),
                    cancellation_token.clone(),
                )
                .await?;
        }
        Ok(())
    }
}

impl ICurrentOutfitPolicy for CompositeCurrentOutfitPolicy {
    fn can_attach(&self, item: Option<Arc<InventoryItem>>) -> Result<bool, Error> {
        self.native_can_attach(item)
    }

    fn can_detach(&self, item: Option<Arc<InventoryItem>>) -> Result<bool, Error> {
        self.native_can_detach(item)
    }

    fn report_item_change(
        &self,
        added: Option<Arc<Vec<InventoryItem>>>,
        removed: Option<Arc<Vec<InventoryItem>>>,
        cancellation_token: Option<CancellationToken>,
    ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + '_>> {
        Box::pin(self.native_report_item_change(added, removed, cancellation_token))
    }
}

#[derive(Clone, Debug)]
struct LinkRequest {
    item: InventoryItem,
    description: String,
}

trait OutfitBackend: Send + Sync {
    fn store(&self) -> Option<Inventory>;
    fn attachments_at(&self, point: AttachmentPoint) -> Vec<InventoryItem>;
    fn attachment_point(&self, item: UUID) -> Option<AttachmentPoint>;
    fn create_links(
        &self,
        parent: UUID,
        links: Vec<LinkRequest>,
    ) -> BackendFuture<'_, Vec<InventoryItem>>;
    fn remove_links(&self, links: Vec<UUID>) -> BackendFuture<'_, ()>;
    fn attach(
        &self,
        item: InventoryItem,
        point: AttachmentPoint,
        replace: bool,
    ) -> BackendFuture<'_, ()>;
    fn detach(&self, item: UUID) -> BackendFuture<'_, ()>;
}

struct ClientOutfitBackend {
    client: ClientWeakHandle,
}

impl ClientOutfitBackend {
    fn client(&self) -> Result<GridClient, Error> {
        self.client.upgrade().ok_or(Error::InvalidOperation)
    }
}

impl OutfitBackend for ClientOutfitBackend {
    fn store(&self) -> Option<Inventory> {
        self.client
            .upgrade()
            .and_then(|client| client.native_inventory().ok())
            .and_then(|manager| manager.native_store())
    }

    fn attachments_at(&self, point: AttachmentPoint) -> Vec<InventoryItem> {
        self.client
            .upgrade()
            .and_then(|client| client.native_appearance().ok())
            .map_or_else(Vec::new, |manager| manager.native_attachments_at(point))
    }

    fn attachment_point(&self, item: UUID) -> Option<AttachmentPoint> {
        self.client
            .upgrade()
            .and_then(|client| client.native_appearance().ok())
            .and_then(|manager| manager.native_attachment_point(item))
    }

    fn create_links(
        &self,
        parent: UUID,
        links: Vec<LinkRequest>,
    ) -> BackendFuture<'_, Vec<InventoryItem>> {
        Box::pin(async move {
            if links.is_empty() {
                return Ok(Vec::new());
            }
            let client = self.client()?;
            let manager = client.native_inventory()?;
            let requested: Vec<_> = links
                .iter()
                .map(|request| (request.item.clone(), request.description.clone()))
                .collect();
            let success = Arc::new(AtomicBool::new(false));
            let callback_success = Arc::clone(&success);
            manager
                .native_create_links(
                    parent,
                    Box::new(requested.into_iter()),
                    Some(Box::new(move |value| {
                        callback_success.store(value, Ordering::Release);
                    })),
                    None,
                )
                .await?;
            if !success.load(Ordering::Acquire) {
                return Err(Error::InvalidOperation);
            }
            let Some(store) = manager.native_store() else {
                return Ok(Vec::new());
            };
            let requested_ids: HashSet<_> = links
                .iter()
                .map(|request| request.item.base.uuid())
                .collect();
            let mut created = Vec::new();
            for target in requested_ids {
                for node in store.find_all_links(target)? {
                    let Some(value) = node.data() else {
                        continue;
                    };
                    let Some(item) = inventory_item(value.as_ref()) else {
                        continue;
                    };
                    if item.base.parent_uuid() == parent {
                        created.push(item);
                    }
                }
            }
            Ok(created)
        })
    }

    fn remove_links(&self, links: Vec<UUID>) -> BackendFuture<'_, ()> {
        Box::pin(async move {
            if links.is_empty() {
                return Ok(());
            }
            self.client()?
                .native_inventory()?
                .native_remove_inventory_objects(links, Vec::new(), None)
                .await
        })
    }

    fn attach(
        &self,
        item: InventoryItem,
        point: AttachmentPoint,
        replace: bool,
    ) -> BackendFuture<'_, ()> {
        Box::pin(async move {
            self.client()?
                .native_appearance()?
                .native_attach(item, point, replace)
        })
    }

    fn detach(&self, item: UUID) -> BackendFuture<'_, ()> {
        Box::pin(async move { self.client()?.native_appearance()?.native_detach(item) })
    }
}

#[derive(Default)]
struct CurrentOutfitState {
    cof: Option<InventoryFolder>,
    shadow_links: HashMap<UUID, InventoryItem>,
    shadow_targets: HashMap<UUID, InventoryItem>,
}

/// Stateful COF service. Mutations are serialized, while policy callbacks and
/// inventory reads are always performed from snapshots outside synchronous locks.
#[derive(Clone)]
pub struct CurrentOutfitFolder {
    backend: Arc<dyn OutfitBackend>,
    policies: Arc<CompositeCurrentOutfitPolicy>,
    state: Arc<RwLock<CurrentOutfitState>>,
    operation: Arc<tokio::sync::Semaphore>,
    disposed: Arc<AtomicBool>,
}

impl CurrentOutfitFolder {
    pub(crate) fn native_new(client: Option<Arc<GridClient>>) -> Result<Self, Error> {
        let client = client.ok_or(Error::ArgumentNull)?;
        let backend: Arc<dyn OutfitBackend> = Arc::new(ClientOutfitBackend {
            client: client.native_weak_handle(),
        });
        Self::with_backend(backend)
    }

    fn with_backend(backend: Arc<dyn OutfitBackend>) -> Result<Self, Error> {
        Ok(Self {
            backend,
            policies: Arc::new(CompositeCurrentOutfitPolicy::native_new()?),
            state: Arc::new(RwLock::new(CurrentOutfitState::default())),
            operation: Arc::new(tokio::sync::Semaphore::new(1)),
            disposed: Arc::new(AtomicBool::new(false)),
        })
    }

    fn ensure_active(&self) -> Result<(), Error> {
        if self.disposed.load(Ordering::Acquire) {
            Err(Error::InvalidOperation)
        } else {
            Ok(())
        }
    }

    async fn acquire_operation<'a>(
        &'a self,
        cancellation_token: Option<&CancellationToken>,
    ) -> Result<tokio::sync::SemaphorePermit<'a>, Error> {
        if let Some(token) = cancellation_token {
            tokio::select! {
                biased;
                () = token.cancelled() => Err(Error::Cancelled),
                permit = self.operation.acquire() => permit.map_err(|_| Error::InvalidOperation),
            }
        } else {
            self.operation
                .acquire()
                .await
                .map_err(|_| Error::InvalidOperation)
        }
    }

    #[allow(clippy::unnecessary_wraps)] // The mapped Dispose API is fallible.
    pub(crate) fn native_dispose(&self) -> Result<(), Error> {
        self.disposed.store(true, Ordering::Release);
        Ok(())
    }

    pub(crate) fn native_add_policy(
        &self,
        policy: Arc<dyn ICurrentOutfitPolicy>,
    ) -> Result<Arc<dyn ICurrentOutfitPolicy>, Error> {
        self.ensure_active()?;
        self.policies.native_add_policy(Some(Arc::clone(&policy)))?;
        Ok(policy)
    }

    pub(crate) fn native_remove_policy(
        &self,
        policy: Arc<dyn ICurrentOutfitPolicy>,
    ) -> Result<(), Error> {
        self.policies.native_remove_policy(Some(policy))
    }

    pub(crate) fn native_cof(&self) -> Option<InventoryFolder> {
        read(&self.state).cof.clone().or_else(|| {
            let store = self.backend.store()?;
            let id = store.find_folder_for_type(FolderType::CurrentOutfit)?;
            store
                .get_value_or_default_with_uuid_a7c63fbe::<InventoryFolder>(id)
                .ok()
                .flatten()
        })
    }

    pub(crate) fn native_set_cof(&mut self, value: Option<InventoryFolder>) {
        write(&self.state).cof = value;
    }

    pub(crate) fn native_resolve_inventory_link(
        &self,
        item: Arc<InventoryItem>,
    ) -> Result<Option<Arc<InventoryItem>>, Error> {
        if !item.is_link()? {
            return Ok(Some(item));
        }
        if let Some(target) = read(&self.state)
            .shadow_targets
            .get(&item.asset_uuid())
            .cloned()
        {
            return Ok(Some(Arc::new(target)));
        }
        let Some(store) = self.backend.store() else {
            return Ok(None);
        };
        Ok(store.native_item_value(item.asset_uuid()).map(Arc::new))
    }

    pub(crate) fn native_get_attachment_item_id(prim: Primitive) -> Result<UUID, Error> {
        let Some(values) = prim.name_values else {
            return Ok(UUID::zero());
        };
        for value in values {
            if value.name != "AttachItemID" {
                continue;
            }
            return match value.value {
                Some(Object::UUID(id)) => Ok(id),
                Some(Object::String(text)) => UUID::parse(text).or(Ok(UUID::zero())),
                _ => Ok(UUID::zero()),
            };
        }
        Ok(UUID::zero())
    }

    #[allow(clippy::unused_async)] // Preserves the mapped asynchronous API boundary.
    pub(crate) async fn native_fetch_parent(
        &self,
        item: InventoryBase,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<Option<InventoryBase>, Error> {
        check_cancelled(cancellation_token.as_ref())?;
        if item.parent_uuid() == UUID::zero() {
            return Ok(None);
        }
        let Some(store) = self.backend.store() else {
            return Ok(None);
        };
        store.get_value_or_default_with_uuid(item.parent_uuid())
    }

    pub(crate) async fn native_is_object_descendent_of(
        &self,
        item: InventoryBase,
        parent_id: UUID,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<bool, Error> {
        if parent_id == UUID::zero() {
            return Ok(false);
        }
        let mut current = item;
        let mut visited = HashSet::new();
        for _ in 0..MAX_ANCESTRY_DEPTH {
            check_cancelled(cancellation_token.as_ref())?;
            let parent = current.parent_uuid();
            if parent == parent_id {
                return Ok(true);
            }
            if parent == UUID::zero() || !visited.insert(parent) {
                return Ok(false);
            }
            let Some(next) = self
                .native_fetch_parent(current, cancellation_token.clone())
                .await?
            else {
                return Ok(false);
            };
            current = next;
        }
        Ok(false)
    }

    #[allow(clippy::unused_async)] // Preserves the mapped asynchronous API boundary.
    pub(crate) async fn native_get_current_outfit_links(
        &self,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<Vec<InventoryItem>, Error> {
        check_cancelled(cancellation_token.as_ref())?;
        let Some(cof) = self.native_cof() else {
            return Ok(Vec::new());
        };
        let mut links = HashMap::new();
        if let Some(store) = self.backend.store()
            && let Ok(contents) = store.get_contents_with_uuid(cof.base.uuid())
        {
            for value in contents {
                if let Some(item) = inventory_item(value.as_ref())
                    && item.is_link()?
                {
                    links.insert(item.base.uuid(), item);
                }
            }
        }
        for (id, item) in &read(&self.state).shadow_links {
            if item.base.parent_uuid() == cof.base.uuid() {
                links.insert(*id, item.clone());
            }
        }
        let mut links: Vec<_> = links.into_values().collect();
        links.sort_unstable_by_key(|item| item.base.uuid());
        Ok(links)
    }

    pub(crate) async fn native_get_worn_at(
        &self,
        wearable_type: WearableType,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<Vec<InventoryItem>, Error> {
        let links = self
            .native_get_current_outfit_links(cancellation_token.clone())
            .await?;
        let mut worn = Vec::new();
        for link in links {
            check_cancelled(cancellation_token.as_ref())?;
            let Some(item) = self.native_resolve_inventory_link(Arc::new(link))? else {
                continue;
            };
            if item_wearable_type(&item) == Some(wearable_type) {
                worn.push((*item).clone());
            }
        }
        Ok(worn)
    }

    #[allow(clippy::unused_async)] // Preserves the mapped asynchronous API boundary.
    pub(crate) async fn native_can_attach_item(
        &self,
        item: InventoryItem,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<bool, Error> {
        self.ensure_active()?;
        check_cancelled(cancellation_token.as_ref())?;
        self.policies.native_can_attach(Some(Arc::new(item)))
    }

    #[allow(clippy::unused_async)] // Preserves the mapped asynchronous API boundary.
    pub(crate) async fn native_can_detach_item(
        &self,
        item: InventoryItem,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<bool, Error> {
        self.ensure_active()?;
        check_cancelled(cancellation_token.as_ref())?;
        self.policies.native_can_detach(Some(Arc::new(item)))
    }

    pub(crate) async fn native_add_to_outfit(
        &self,
        requested: Vec<InventoryItem>,
        replace: bool,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<(), Error> {
        self.ensure_active()?;
        check_cancelled(cancellation_token.as_ref())?;
        let operation = self.acquire_operation(cancellation_token.as_ref()).await?;
        check_cancelled(cancellation_token.as_ref())?;
        let changes = self
            .plan_add(requested, replace, cancellation_token.clone())
            .await?;
        let (added, removed) = self.commit_changes(changes).await?;
        drop(operation);
        self.report_changes(added, removed, cancellation_token)
            .await
    }

    pub(crate) async fn native_remove_from_outfit(
        &self,
        requested: Vec<InventoryItem>,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<(), Error> {
        self.ensure_active()?;
        check_cancelled(cancellation_token.as_ref())?;
        let operation = self.acquire_operation(cancellation_token.as_ref()).await?;
        check_cancelled(cancellation_token.as_ref())?;
        let links = self
            .native_get_current_outfit_links(cancellation_token.clone())
            .await?;
        let requested_ids: HashSet<_> = requested
            .iter()
            .flat_map(|item| [item.base.uuid(), item.resolved_item_id()])
            .collect();
        let remove: Vec<_> = links
            .into_iter()
            .filter(|link| {
                requested_ids.contains(&link.base.uuid())
                    || requested_ids.contains(&link.asset_uuid())
            })
            .collect();
        self.backend
            .remove_links(remove.iter().map(|link| link.base.uuid()).collect())
            .await?;
        self.remove_shadow(&remove);
        drop(operation);
        self.report_changes(Vec::new(), requested, cancellation_token)
            .await
    }

    pub(crate) async fn native_attach(
        &self,
        item: InventoryItem,
        point: AttachmentPoint,
        replace: bool,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<(), Error> {
        if !self
            .native_can_attach_item(item.clone(), cancellation_token.clone())
            .await?
        {
            return Err(Error::InvalidOperation);
        }
        let operation = self.acquire_operation(cancellation_token.as_ref()).await?;
        check_cancelled(cancellation_token.as_ref())?;
        let Some(cof) = self.native_cof() else {
            return Err(Error::InvalidOperation);
        };
        let displaced = if replace && point != AttachmentPoint::Default {
            self.backend.attachments_at(point)
        } else {
            Vec::new()
        };
        let displaced_ids: HashSet<_> = displaced.iter().map(|item| item.base.uuid()).collect();
        let displaced_links: Vec<_> = self
            .native_get_current_outfit_links(cancellation_token.clone())
            .await?
            .into_iter()
            .filter(|link| displaced_ids.contains(&link.asset_uuid()))
            .collect();
        let created = self
            .backend
            .create_links(
                cof.base.uuid(),
                vec![LinkRequest {
                    item: item.clone(),
                    description: String::new(),
                }],
            )
            .await?;
        if let Err(error) = self.backend.attach(item.clone(), point, replace).await {
            let _ = self
                .backend
                .remove_links(created.iter().map(|link| link.base.uuid()).collect())
                .await;
            return Err(error);
        }
        let remove_result = if displaced_links.is_empty() {
            Ok(())
        } else {
            self.backend
                .remove_links(
                    displaced_links
                        .iter()
                        .map(|link| link.base.uuid())
                        .collect(),
                )
                .await
        };
        if let Err(error) = remove_result {
            let _ = self
                .backend
                .remove_links(created.iter().map(|link| link.base.uuid()).collect())
                .await;
            let _ = self.backend.detach(item.base.uuid()).await;
            for old in displaced {
                let _ = self.backend.attach(old, point, false).await;
            }
            return Err(error);
        }
        self.add_shadow(
            &created,
            &[LinkRequest {
                item: item.clone(),
                description: String::new(),
            }],
        );
        self.remove_shadow(&displaced_links);
        drop(operation);
        self.report_changes(vec![item], displaced, cancellation_token)
            .await
    }

    pub(crate) async fn native_detach(
        &self,
        item: InventoryItem,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<(), Error> {
        if !self
            .native_can_detach_item(item.clone(), cancellation_token.clone())
            .await?
        {
            return Err(Error::InvalidOperation);
        }
        let operation = self.acquire_operation(cancellation_token.as_ref()).await?;
        check_cancelled(cancellation_token.as_ref())?;
        let attachment_point = self.backend.attachment_point(item.base.uuid());
        let links = self
            .native_get_current_outfit_links(cancellation_token.clone())
            .await?;
        let remove: Vec<_> = links
            .into_iter()
            .filter(|link| link.asset_uuid() == item.base.uuid())
            .collect();
        self.backend.detach(item.base.uuid()).await?;
        if let Err(error) = self
            .backend
            .remove_links(remove.iter().map(|link| link.base.uuid()).collect())
            .await
        {
            if let Some(point) = attachment_point {
                let _ = self.backend.attach(item, point, false).await;
            }
            return Err(error);
        }
        self.remove_shadow(&remove);
        drop(operation);
        self.report_changes(Vec::new(), vec![item], cancellation_token)
            .await
    }

    pub(crate) async fn native_replace_outfit(
        &self,
        folder_id: UUID,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<bool, Error> {
        self.ensure_active()?;
        check_cancelled(cancellation_token.as_ref())?;
        let Some(store) = self.backend.store() else {
            return Ok(false);
        };
        let Ok(contents) = store.get_contents_with_uuid(folder_id) else {
            return Ok(false);
        };
        let mut requested = Vec::new();
        for value in contents {
            let Some(item) = inventory_item(value.as_ref()) else {
                continue;
            };
            if item.is_link()? {
                if let Some(resolved) = self.native_resolve_inventory_link(Arc::new(item))? {
                    requested.push((*resolved).clone());
                }
            } else {
                requested.push(item);
            }
        }
        let requested_ids: HashSet<_> = requested.iter().map(|item| item.base.uuid()).collect();
        let operation = self.acquire_operation(cancellation_token.as_ref()).await?;
        check_cancelled(cancellation_token.as_ref())?;
        let mut changes = self
            .plan_add(requested, true, cancellation_token.clone())
            .await?;
        for link in self
            .native_get_current_outfit_links(cancellation_token.clone())
            .await?
        {
            if !requested_ids.contains(&link.asset_uuid()) {
                changes.remove.push(link);
            }
        }
        changes.remove.sort_unstable_by_key(|item| item.base.uuid());
        changes.remove.dedup_by_key(|item| item.base.uuid());
        let (added, removed) = self.commit_changes(changes).await?;
        drop(operation);
        self.report_changes(added, removed, cancellation_token)
            .await?;
        Ok(true)
    }

    async fn plan_add(
        &self,
        requested: Vec<InventoryItem>,
        replace: bool,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<OutfitChanges, Error> {
        let Some(cof) = self.native_cof() else {
            return Err(Error::InvalidOperation);
        };
        let links = self
            .native_get_current_outfit_links(cancellation_token.clone())
            .await?;
        let mut current = Vec::new();
        for link in &links {
            if let Some(item) = self.native_resolve_inventory_link(Arc::new(link.clone()))? {
                current.push((link.clone(), (*item).clone()));
            }
        }
        let mut seen = HashSet::new();
        let mut add: Vec<LinkRequest> = Vec::new();
        let mut remove = Vec::new();
        let mut planned_layers = HashMap::<WearableType, HashSet<i32>>::new();
        for (link, worn) in &current {
            if let Some(wearable_type) = item_wearable_type(worn)
                && let Some(layer) = parse_layer(&link.description(), wearable_type)
            {
                planned_layers
                    .entry(wearable_type)
                    .or_default()
                    .insert(layer);
            }
        }
        let mut replaced_types = HashSet::new();
        for item in requested {
            check_cancelled(cancellation_token.as_ref())?;
            if !seen.insert(item.base.uuid())
                || current
                    .iter()
                    .any(|(_, worn)| worn.base.uuid() == item.base.uuid())
            {
                continue;
            }
            let description = if let Some(wearable_type) = item_wearable_type(&item) {
                let same_type: Vec<_> = current
                    .iter()
                    .filter(|(_, worn)| item_wearable_type(worn) == Some(wearable_type))
                    .collect();
                let bodypart = item.asset_type() == AssetType::Bodypart;
                if (replace && replaced_types.insert(wearable_type)) || bodypart {
                    remove.extend(same_type.iter().map(|(link, _)| link.clone()));
                }
                if bodypart {
                    add.retain(|request| item_wearable_type(&request.item) != Some(wearable_type));
                    String::new()
                } else {
                    if replace
                        && replaced_types.contains(&wearable_type)
                        && !add
                            .iter()
                            .any(|request| item_wearable_type(&request.item) == Some(wearable_type))
                    {
                        planned_layers.entry(wearable_type).or_default().clear();
                    }
                    let used = planned_layers.entry(wearable_type).or_default();
                    let layer = (0..MAX_CLOTHING_LAYERS)
                        .find(|layer| !used.contains(layer))
                        .ok_or(Error::InvalidOperation)?;
                    used.insert(layer);
                    format!("{}{:02}", wearable_type as u8, layer)
                }
            } else if is_attachment(&item) {
                String::new()
            } else {
                return Err(Error::Argument);
            };
            add.push(LinkRequest { item, description });
        }
        remove.sort_unstable_by_key(|item| item.base.uuid());
        remove.dedup_by_key(|item| item.base.uuid());
        Ok(OutfitChanges {
            cof: cof.base.uuid(),
            add,
            remove,
        })
    }

    async fn commit_changes(
        &self,
        changes: OutfitChanges,
    ) -> Result<(Vec<InventoryItem>, Vec<InventoryItem>), Error> {
        let added_items: Vec<_> = changes.add.iter().map(|entry| entry.item.clone()).collect();
        let removed_items: Vec<_> = changes
            .remove
            .iter()
            .filter_map(|link| {
                self.native_resolve_inventory_link(Arc::new(link.clone()))
                    .ok()
                    .flatten()
                    .map(|item| (*item).clone())
            })
            .collect();
        let created = self
            .backend
            .create_links(changes.cof, changes.add.clone())
            .await?;
        if let Err(error) = self
            .backend
            .remove_links(changes.remove.iter().map(|link| link.base.uuid()).collect())
            .await
        {
            let _ = self
                .backend
                .remove_links(created.iter().map(|link| link.base.uuid()).collect())
                .await;
            let restore: Vec<_> = changes
                .remove
                .iter()
                .filter_map(|link| {
                    self.native_resolve_inventory_link(Arc::new(link.clone()))
                        .ok()
                        .flatten()
                        .map(|item| LinkRequest {
                            item: (*item).clone(),
                            description: link.description(),
                        })
                })
                .collect();
            let _ = self.backend.create_links(changes.cof, restore).await;
            return Err(error);
        }
        self.add_shadow(&created, &changes.add);
        self.remove_shadow(&changes.remove);
        Ok((added_items, removed_items))
    }

    async fn report_changes(
        &self,
        added: Vec<InventoryItem>,
        removed: Vec<InventoryItem>,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<(), Error> {
        // A committed mutation is never rolled back merely because cancellation
        // races with notification. Policies receive the committed change exactly once.
        let notification_token =
            cancellation_token.filter(|token| !token.is_cancellation_requested());
        self.policies
            .native_report_item_change(
                Some(Arc::new(added)),
                Some(Arc::new(removed)),
                notification_token,
            )
            .await
    }

    fn add_shadow(&self, links: &[InventoryItem], requests: &[LinkRequest]) {
        let mut state = write(&self.state);
        for link in links {
            state.shadow_links.insert(link.base.uuid(), link.clone());
        }
        for request in requests {
            state
                .shadow_targets
                .insert(request.item.base.uuid(), request.item.clone());
        }
    }

    fn remove_shadow(&self, links: &[InventoryItem]) {
        let mut state = write(&self.state);
        for link in links {
            state.shadow_links.remove(&link.base.uuid());
        }
        let retained: HashSet<_> = state
            .shadow_links
            .values()
            .map(InventoryItem::asset_uuid)
            .collect();
        state
            .shadow_targets
            .retain(|target, _| retained.contains(target));
    }

    #[allow(dead_code, clippy::unnecessary_wraps, clippy::unused_self)] // The compatibility overload is fallible and instance-based.
    pub(crate) fn get_cof_link_description<T: CofLinkItem>(
        &self,
        item: &T,
    ) -> Result<String, Error> {
        if item.inventory_item().asset_type() == AssetType::Bodypart {
            return Ok(String::new());
        }
        Ok(item
            .wearable_type()
            .map_or_else(String::new, |value| format!("{}00", value as u8)))
    }
}

struct OutfitChanges {
    cof: UUID,
    add: Vec<LinkRequest>,
    remove: Vec<InventoryItem>,
}

#[allow(dead_code)] // Internal C# overload adapter used by compatibility fixtures.
pub(crate) trait CofLinkItem {
    fn inventory_item(&self) -> &InventoryItem;
    fn wearable_type(&self) -> Option<WearableType>;
}

impl CofLinkItem for InventoryWearable {
    fn inventory_item(&self) -> &InventoryItem {
        &self.base
    }

    fn wearable_type(&self) -> Option<WearableType> {
        Some(self.wearable_type())
    }
}

impl CofLinkItem for InventoryObject {
    fn inventory_item(&self) -> &InventoryItem {
        &self.base
    }

    fn wearable_type(&self) -> Option<WearableType> {
        None
    }
}

fn inventory_item(value: &dyn InventoryObjectClass) -> Option<InventoryItem> {
    if let Some(item) = value.as_any().downcast_ref::<InventoryItem>() {
        return Some(item.clone());
    }
    macro_rules! item {
        ($kind:ty) => {
            if let Some(item) = value.as_any().downcast_ref::<$kind>() {
                return Some(item.base.clone());
            }
        };
    }
    item!(crate::InventoryAnimation);
    item!(crate::InventoryAttachment);
    item!(crate::InventoryCallingCard);
    item!(crate::InventoryCategory);
    item!(crate::InventoryGesture);
    item!(crate::InventoryLSL);
    item!(crate::InventoryLandmark);
    item!(crate::InventoryMaterial);
    item!(crate::InventoryNotecard);
    item!(crate::InventoryObject);
    item!(crate::InventorySettings);
    item!(crate::InventorySnapshot);
    item!(crate::InventorySound);
    item!(crate::InventoryTexture);
    item!(crate::InventoryWearable);
    None
}

fn item_wearable_type(item: &InventoryItem) -> Option<WearableType> {
    matches!(item.asset_type(), AssetType::Bodypart | AssetType::Clothing).then(|| {
        let wearable = InventoryWearable { base: item.clone() };
        wearable.wearable_type()
    })
}

fn is_attachment(item: &InventoryItem) -> bool {
    matches!(item.asset_type(), AssetType::Object | AssetType::Mesh)
}

fn parse_layer(description: &str, wearable_type: WearableType) -> Option<i32> {
    let prefix = (wearable_type as u8).to_string();
    description
        .strip_prefix(&prefix)?
        .parse::<i32>()
        .ok()
        .filter(|layer| (0..MAX_CLOTHING_LAYERS).contains(layer))
}

#[allow(clippy::too_many_lines)] // Mirrors every field in the mapped constructor.
pub(crate) fn primitive_defaults() -> Primitive {
    Primitive {
        acceleration: libremetaverse_types::Vector3::zero(),
        active_clients: 0,
        angular_velocity: libremetaverse_types::Vector3::zero(),
        crc: 0,
        child_count: 0,
        click_action: libremetaverse_types::ClickAction::Touch,
        collision_plane: libremetaverse_types::Vector4::zero(),
        extended_mesh_flags: 0,
        face_media: Vec::new(),
        flags: libremetaverse_types::PrimFlags::NONE,
        flexible: None,
        group_id: UUID::zero(),
        id: UUID::zero(),
        is_attachment: false,
        joint: libremetaverse_types::JointType::Invalid,
        joint_axis_or_anchor: libremetaverse_types::Vector3::zero(),
        joint_pivot: libremetaverse_types::Vector3::zero(),
        light: None,
        light_map: None,
        local_id: 0,
        media_url: String::new(),
        media_version: String::new(),
        name_values: None,
        owner_id: UUID::zero(),
        parent_id: 0,
        particle_sys: crate::PrimitiveParticleSystem {
            angular_velocity: libremetaverse_types::Vector3::zero(),
            blend_func_dest: crate::PrimitiveParticleSystemBlendFunc::OneMinusSourceAlpha as u8,
            blend_func_source: crate::PrimitiveParticleSystemBlendFunc::SourceAlpha as u8,
            burst_part_count: 0,
            burst_radius: 0.0,
            burst_rate: 0.0,
            burst_speed_max: 0.0,
            burst_speed_min: 0.0,
            crc: 0,
            inner_angle: 0.0,
            max_age: 0.0,
            outer_angle: 0.0,
            part_acceleration: libremetaverse_types::Vector3::zero(),
            part_data_flags: crate::PrimitiveParticleSystemParticleDataFlags::NONE,
            part_end_color: libremetaverse_types::Color4::white(),
            part_end_glow: 0.0,
            part_end_scale_x: 1.0,
            part_end_scale_y: 1.0,
            part_flags: 0,
            part_max_age: 0.0,
            part_start_color: libremetaverse_types::Color4::white(),
            part_start_glow: 0.0,
            part_start_scale_x: 1.0,
            part_start_scale_y: 1.0,
            pattern: crate::PrimitiveParticleSystemSourcePattern::NONE,
            start_age: 0.0,
            target: UUID::zero(),
            texture: UUID::zero(),
        },
        physics_props: None,
        position: libremetaverse_types::Vector3::zero(),
        prim_data: crate::PrimitiveConstructionData {
            material: libremetaverse_types::Material::Stone,
            p_code: libremetaverse_types::PCode::Prim,
            path_begin: 0.0,
            path_curve: libremetaverse_types::PathCurve::Line,
            path_end: 1.0,
            path_radius_offset: 0.0,
            path_revolutions: 1.0,
            path_scale_x: 1.0,
            path_scale_y: 1.0,
            path_shear_x: 0.0,
            path_shear_y: 0.0,
            path_skew: 0.0,
            path_taper_x: 0.0,
            path_taper_y: 0.0,
            path_twist: 0.0,
            path_twist_begin: 0.0,
            profile_begin: 0.0,
            profile_end: 1.0,
            profile_hollow: 0.0,
            state: 0,
            profile_curve_with_field: 0,
        },
        properties: None,
        region_handle: 0,
        rotation: libremetaverse_types::Quaternion::identity(),
        scale: libremetaverse_types::Vector3::one(),
        scratch_pad: Vec::new(),
        sculpt: None,
        signaled_animations: None,
        sound: UUID::zero(),
        sound_flags: libremetaverse_types::SoundFlags::NONE,
        sound_gain: 0.0,
        sound_radius: 0.0,
        tag: None,
        text: String::new(),
        text_color: libremetaverse_types::Color4::black(),
        texture_anim: crate::PrimitiveTextureAnimation {
            face: 0,
            flags: crate::PrimitiveTextureAnimMode::ANIM_OFF,
            length: 0.0,
            rate: 0.0,
            size_x: 0,
            size_y: 0,
            start: 0.0,
        },
        textures: None,
        tree_species: crate::Tree::Pine1,
        unknown_extra_params: Vec::new(),
        velocity: libremetaverse_types::Vector3::zero(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use libremetaverse_types::InventoryType;
    use libremetaverse_types::compat::CancellationTokenSource;
    use std::sync::Mutex;
    use std::sync::atomic::AtomicU64;

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

    #[derive(Default)]
    struct RecordingBackend {
        events: Mutex<Vec<String>>,
        next: AtomicU64,
        fail_remove: AtomicBool,
        attachments: RwLock<Vec<(InventoryItem, AttachmentPoint)>>,
    }

    impl RecordingBackend {
        fn events(&self) -> Vec<String> {
            mutex(&self.events).clone()
        }
    }

    impl OutfitBackend for RecordingBackend {
        fn store(&self) -> Option<Inventory> {
            None
        }

        fn attachments_at(&self, point: AttachmentPoint) -> Vec<InventoryItem> {
            read(&self.attachments)
                .iter()
                .filter(|(_, candidate)| *candidate == point)
                .map(|(item, _)| item.clone())
                .collect()
        }

        fn attachment_point(&self, item: UUID) -> Option<AttachmentPoint> {
            read(&self.attachments)
                .iter()
                .find(|(candidate, _)| candidate.base.uuid() == item)
                .map(|(_, point)| *point)
        }

        fn create_links(
            &self,
            parent: UUID,
            links: Vec<LinkRequest>,
        ) -> BackendFuture<'_, Vec<InventoryItem>> {
            Box::pin(async move {
                let mut created = Vec::new();
                for request in links {
                    mutex(&self.events).push(format!(
                        "create:{}:{}",
                        request.item.base.name(),
                        request.description
                    ));
                    let ordinal = self.next.fetch_add(1, Ordering::AcqRel) + 1;
                    let mut link = InventoryItem::new_with_uuid(UUID::new_with_u_int64(ordinal)?)?;
                    link.base.set_parent_uuid(parent);
                    link.base.set_name(request.item.base.name());
                    link.set_asset_type(AssetType::Link);
                    link.set_inventory_type(request.item.inventory_type());
                    link.set_asset_uuid(request.item.base.uuid());
                    link.set_description(request.description);
                    created.push(link);
                }
                Ok(created)
            })
        }

        fn remove_links(&self, links: Vec<UUID>) -> BackendFuture<'_, ()> {
            Box::pin(async move {
                mutex(&self.events).push(format!("remove:{}", links.len()));
                if self.fail_remove.load(Ordering::Acquire) && !links.is_empty() {
                    Err(Error::InvalidOperation)
                } else {
                    Ok(())
                }
            })
        }

        fn attach(
            &self,
            item: InventoryItem,
            point: AttachmentPoint,
            replace: bool,
        ) -> BackendFuture<'_, ()> {
            Box::pin(async move {
                mutex(&self.events).push(format!(
                    "attach:{}:{}:{replace}",
                    item.base.name(),
                    point as u8
                ));
                let mut attachments = write(&self.attachments);
                if replace && point != AttachmentPoint::Default {
                    attachments.retain(|(_, candidate)| *candidate != point);
                }
                attachments.retain(|(existing, _)| existing.base.uuid() != item.base.uuid());
                attachments.push((item, point));
                Ok(())
            })
        }

        fn detach(&self, item: UUID) -> BackendFuture<'_, ()> {
            Box::pin(async move {
                mutex(&self.events).push(format!("detach:{item}"));
                write(&self.attachments).retain(|(existing, _)| existing.base.uuid() != item);
                Ok(())
            })
        }
    }

    fn block_on<F: Future>(future: F) -> F::Output {
        use std::task::{Context, Poll, Wake, Waker};
        struct ThreadWake(std::thread::Thread);
        impl Wake for ThreadWake {
            fn wake(self: Arc<Self>) {
                self.0.unpark();
            }
        }
        let waker = Waker::from(Arc::new(ThreadWake(std::thread::current())));
        let mut context = Context::from_waker(&waker);
        let mut future = std::pin::pin!(future);
        loop {
            match future.as_mut().poll(&mut context) {
                Poll::Ready(output) => return output,
                Poll::Pending => std::thread::park(),
            }
        }
    }

    fn service(backend: Arc<RecordingBackend>) -> CurrentOutfitFolder {
        let backend_dyn: Arc<dyn OutfitBackend> = backend;
        let service = CurrentOutfitFolder::with_backend(backend_dyn).unwrap();
        let mut folder = InventoryFolder::new(UUID::new_with_u_int64(900).unwrap()).unwrap();
        folder.base.set_name("Current Outfit".into());
        write(&service.state).cof = Some(folder);
        service
    }

    fn wearable(name: &str, id: u64, wearable_type: WearableType) -> InventoryItem {
        let mut item = InventoryWearable::new(UUID::new_with_u_int64(id).unwrap()).unwrap();
        item.base.base.set_name(name.into());
        item.base
            .set_asset_uuid(UUID::new_with_u_int64(id + 100).unwrap());
        item.base.set_asset_type(AssetType::Clothing);
        item.base.set_inventory_type(InventoryType::WEARABLE);
        item.set_wearable_type(wearable_type);
        item.base
    }

    fn attachment(name: &str, id: u64) -> InventoryItem {
        let mut item = InventoryObject::new(UUID::new_with_u_int64(id).unwrap()).unwrap();
        item.base.base.set_name(name.into());
        item.base.set_asset_type(AssetType::Object);
        item.base.set_inventory_type(InventoryType::OBJECT);
        item.base
    }

    #[test]
    fn fake_grid_add_preserves_layers_and_worn_state() {
        let backend = Arc::new(RecordingBackend::default());
        let service = service(Arc::clone(&backend));
        block_on(service.native_add_to_outfit(
            vec![
                wearable("first", 10, WearableType::Shirt),
                wearable("second", 11, WearableType::Shirt),
            ],
            false,
            None,
        ))
        .unwrap();
        assert_eq!(
            backend.events(),
            ["create:first:400", "create:second:401", "remove:0"]
        );
        let worn = block_on(service.native_get_worn_at(WearableType::Shirt, None)).unwrap();
        assert_eq!(worn.len(), 2);
    }

    #[test]
    fn failed_replace_keeps_old_shadow_state_and_records_compensation() {
        let backend = Arc::new(RecordingBackend::default());
        let service = service(Arc::clone(&backend));
        let old = wearable("old", 20, WearableType::Shirt);
        let request = LinkRequest {
            item: old.clone(),
            description: "400".into(),
        };
        let created = block_on(
            backend.create_links(UUID::new_with_u_int64(900).unwrap(), vec![request.clone()]),
        )
        .unwrap();
        service.add_shadow(&created, &[request]);
        mutex(&backend.events).clear();
        backend.fail_remove.store(true, Ordering::Release);
        assert_eq!(
            block_on(service.native_add_to_outfit(
                vec![wearable("new", 21, WearableType::Shirt)],
                true,
                None,
            )),
            Err(Error::InvalidOperation)
        );
        assert_eq!(
            backend.events(),
            ["create:new:400", "remove:1", "remove:1", "create:old:400"]
        );
        let worn = block_on(service.native_get_worn_at(WearableType::Shirt, None)).unwrap();
        assert_eq!(worn, vec![old]);
    }

    #[test]
    fn attach_creates_cof_link_before_network_execution() {
        let backend = Arc::new(RecordingBackend::default());
        let service = service(Arc::clone(&backend));
        block_on(service.native_attach(attachment("hat", 30), AttachmentPoint::Skull, true, None))
            .unwrap();
        assert_eq!(backend.events(), ["create:hat:", "attach:hat:2:true"]);
        assert_eq!(
            block_on(service.native_get_current_outfit_links(None))
                .unwrap()
                .len(),
            1
        );
    }

    #[test]
    fn attachment_replace_removes_displaced_link_after_new_attachment() {
        let backend = Arc::new(RecordingBackend::default());
        let service = service(Arc::clone(&backend));
        let old = attachment("old-hat", 31);
        block_on(backend.attach(old.clone(), AttachmentPoint::Skull, false)).unwrap();
        let request = LinkRequest {
            item: old,
            description: String::new(),
        };
        let created = block_on(
            backend.create_links(UUID::new_with_u_int64(900).unwrap(), vec![request.clone()]),
        )
        .unwrap();
        service.add_shadow(&created, &[request]);
        mutex(&backend.events).clear();

        block_on(service.native_attach(
            attachment("new-hat", 32),
            AttachmentPoint::Skull,
            true,
            None,
        ))
        .unwrap();

        assert_eq!(
            backend.events(),
            ["create:new-hat:", "attach:new-hat:2:true", "remove:1"]
        );
        let attachments = backend.attachments_at(AttachmentPoint::Skull);
        assert_eq!(attachments.len(), 1);
        assert_eq!(attachments[0].base.name(), "new-hat");
        let links = block_on(service.native_get_current_outfit_links(None)).unwrap();
        assert_eq!(links.len(), 1);
        assert_eq!(links[0].asset_uuid(), attachments[0].base.uuid());
    }

    #[test]
    fn cancelled_operation_does_not_reach_backend() {
        let backend = Arc::new(RecordingBackend::default());
        let service = service(Arc::clone(&backend));
        let source = CancellationTokenSource::new();
        source.cancel();
        assert_eq!(
            block_on(service.native_add_to_outfit(
                vec![wearable("shirt", 40, WearableType::Shirt)],
                false,
                Some(source.token()),
            )),
            Err(Error::Cancelled)
        );
        assert!(backend.events().is_empty());
    }
}
