//! Native Inventory API v3 capability client and cache reconciliation.

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

use crate::client_core::ClientWeakHandle;
use crate::{Error, GridClient, InventoryFolder, InventoryItem};
use libremetaverse_structured_data::{OSD, OSDMap, OSDParser};
use libremetaverse_types::compat::{CancellationToken, HttpResponse, Uri};
use libremetaverse_types::{AssetType, UUID};
use std::collections::{BTreeMap, HashMap, HashSet};
use std::sync::Arc;
use std::time::{Duration, Instant};

const MAX_FOLDER_DEPTH_REQUEST: i32 = 50;
const MAX_AIS_OBJECTS: usize = 100_000;
const LLSD_XML: &str = "application/llsd+xml";

type InventoryResult = (
    bool,
    Vec<InventoryFolder>,
    Vec<InventoryItem>,
    Vec<InventoryItem>,
);
type ParsedInventory = (Vec<InventoryFolder>, Vec<InventoryItem>, Vec<InventoryItem>);
type CollectionSections<'a> = (Option<&'a HashMap<String, OSD>>, Option<&'a OSD>);

#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct AISResponseMeta {
    broken_links_removed: Vec<UUID>,
    items_removed: Vec<UUID>,
    categories_removed: Vec<UUID>,
    category_version_updates: HashMap<UUID, i32>,
}

impl AISResponseMeta {
    pub fn new(
        broken_links: Vec<UUID>,
        items: Vec<UUID>,
        categories: Vec<UUID>,
        versions: HashMap<UUID, i32>,
    ) -> Result<Self, Error> {
        Ok(Self {
            broken_links_removed: broken_links,
            items_removed: items,
            categories_removed: categories,
            category_version_updates: versions,
        })
    }

    #[must_use]
    pub fn empty() -> Self {
        Self::default()
    }

    #[must_use]
    pub fn broken_links_removed(&self) -> Vec<UUID> {
        self.broken_links_removed.clone()
    }

    #[must_use]
    pub fn items_removed(&self) -> Vec<UUID> {
        self.items_removed.clone()
    }

    #[must_use]
    pub fn categories_removed(&self) -> Vec<UUID> {
        self.categories_removed.clone()
    }

    #[must_use]
    pub fn category_version_updates(&self) -> HashMap<UUID, i32> {
        self.category_version_updates.clone()
    }

    #[must_use]
    pub fn has_any_data(&self) -> bool {
        !self.broken_links_removed.is_empty()
            || !self.items_removed.is_empty()
            || !self.categories_removed.is_empty()
            || !self.category_version_updates.is_empty()
    }
}

#[derive(Clone)]
pub struct InventoryAISClient {
    client: ClientWeakHandle,
}

impl InventoryAISClient {
    pub(crate) fn native_new(client: Option<Arc<GridClient>>) -> Result<Self, Error> {
        let client = client.ok_or(Error::ArgumentNull)?;
        Ok(Self {
            client: client.native_weak_handle(),
        })
    }

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

    fn capability(&self, name: &str) -> Result<Option<Uri>, Error> {
        let client = self.client()?;
        let Some(simulator) = client.native_network()?.current_sim() else {
            return Ok(None);
        };
        let Some(caps) = simulator.native_caps() else {
            return Ok(None);
        };
        caps.capability_uri(name.to_owned())
    }

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

    pub(crate) fn native_is_available(&self) -> bool {
        self.capability(Self::INVENTORY_CAP_NAME)
            .ok()
            .flatten()
            .is_some()
    }

    fn endpoint(capability: &Uri, path: &str) -> Uri {
        Uri(format!("{}/{}", capability.0.trim_end_matches('/'), path))
    }

    fn valid_category(category: &str) -> bool {
        !category.is_empty()
            && category
                .bytes()
                .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
    }

    fn token(token: Option<CancellationToken>) -> CancellationToken {
        token.unwrap_or_default()
    }

    fn status_ok(response: &HttpResponse) -> bool {
        response.is_success_status_code() || response.status_code == 304
    }

    async fn request(
        &self,
        cap_name: &str,
        method: &str,
        path: String,
        headers: BTreeMap<String, String>,
        payload: Option<OSD>,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<Option<(HttpResponse, Vec<u8>)>, Error> {
        let token = Self::token(cancellation_token);
        token.throw_if_cancellation_requested()?;
        let Some(capability) = self.wait_for_capability(cap_name, &token)? else {
            return Ok(None);
        };
        let bytes = match payload {
            Some(payload) => OSDParser::serialize_llsd_xml_bytes(payload)?,
            None => Vec::new(),
        };
        let content_type = (!bytes.is_empty()).then(|| LLSD_XML.to_owned());
        self.client()?
            .native_http_caps_client()
            .send_custom(
                method,
                Self::endpoint(&capability, &path),
                headers,
                content_type,
                bytes,
                token,
            )
            .await
            .map(Some)
    }

    fn decode_map(bytes: Vec<u8>) -> Result<Option<HashMap<String, OSD>>, Error> {
        if bytes.is_empty() {
            return Ok(None);
        }
        match OSDParser::deserialize_with_bytes(bytes)? {
            OSD::Map(map) => Ok(Some(map)),
            _ => Err(Error::Argument),
        }
    }

    fn parse_all_map(&self, map: &HashMap<String, OSD>) -> Result<ParsedInventory, Error> {
        let response = OSD::Map(map.clone());
        let folders = self.parse_folders_osd(&response)?;
        let items = self.parse_items_osd(&response)?;
        let links = self.parse_links_osd(&response)?;
        if folders.len() + items.len() + links.len() > MAX_AIS_OBJECTS {
            return Err(Error::InvalidOperation);
        }
        Ok((folders, items, links))
    }

    fn reconcile(
        &self,
        map: &HashMap<String, OSD>,
        folders: &[InventoryFolder],
        items: &[InventoryItem],
        links: &[InventoryItem],
    ) -> Result<(), Error> {
        let meta = Self::parse_meta_map(map)?;
        if folders.is_empty() && items.is_empty() && links.is_empty() && !meta.has_any_data() {
            return Ok(());
        }
        self.client()?.native_inventory()?.native_reconcile_ais(
            folders.to_vec(),
            items.iter().chain(links).cloned().collect(),
            &meta,
        )
    }

    async fn mutation(
        &self,
        cap_name: &str,
        method: &str,
        path: String,
        headers: BTreeMap<String, String>,
        payload: Option<OSD>,
        gone_is_success: bool,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<
        (
            bool,
            Vec<InventoryFolder>,
            Vec<InventoryItem>,
            Vec<InventoryItem>,
        ),
        Error,
    > {
        let Some((response, bytes)) = self
            .request(cap_name, method, path, headers, payload, cancellation_token)
            .await?
        else {
            return Ok((false, Vec::new(), Vec::new(), Vec::new()));
        };
        let success =
            Self::status_ok(&response) || (gone_is_success && response.status_code == 410);
        if !success {
            return Ok((false, Vec::new(), Vec::new(), Vec::new()));
        }
        let Some(map) = Self::decode_map(bytes)? else {
            return Ok((true, Vec::new(), Vec::new(), Vec::new()));
        };
        let (folders, items, links) = self.parse_all_map(&map)?;
        self.reconcile(&map, &folders, &items, &links)?;
        Ok((true, folders, items, links))
    }

    async fn mutation_bool(
        &self,
        cap_name: &str,
        method: &str,
        path: String,
        headers: BTreeMap<String, String>,
        payload: Option<OSD>,
        gone_is_success: bool,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<bool, Error> {
        match self
            .mutation(
                cap_name,
                method,
                path,
                headers,
                payload,
                gone_is_success,
                cancellation_token,
            )
            .await
        {
            Ok((success, _, _, _)) => Ok(success),
            Err(Error::Cancelled) => Err(Error::Cancelled),
            Err(error) => {
                if cfg!(test) {
                    eprintln!("AIS mutation failed: {error:?}");
                }
                Ok(false)
            }
        }
    }

    async fn get_parsed(
        &self,
        cap_name: &str,
        path: String,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<InventoryResult, Error> {
        let result = async {
            let Some((response, bytes)) = self
                .request(
                    cap_name,
                    "GET",
                    path,
                    BTreeMap::new(),
                    None,
                    cancellation_token,
                )
                .await?
            else {
                return Ok((false, Vec::new(), Vec::new(), Vec::new()));
            };
            if !Self::status_ok(&response) {
                return Ok((false, Vec::new(), Vec::new(), Vec::new()));
            }
            let Some(map) = Self::decode_map(bytes)? else {
                return Ok((true, Vec::new(), Vec::new(), Vec::new()));
            };
            let parsed = self.parse_all_map(&map)?;
            self.reconcile(&map, &parsed.0, &parsed.1, &parsed.2)?;
            Ok((true, parsed.0, parsed.1, parsed.2))
        }
        .await;
        match result {
            Err(Error::Cancelled) => Err(Error::Cancelled),
            Err(_) => Ok((false, Vec::new(), Vec::new(), Vec::new())),
            value => value,
        }
    }

    fn tid() -> Result<UUID, Error> {
        UUID::random()
    }

    fn destination(uuid: UUID) -> BTreeMap<String, String> {
        BTreeMap::from([("Destination".to_owned(), uuid.to_string())])
    }

    pub(crate) async fn native_create_inventory(
        &self,
        parent_uuid: UUID,
        new_inventory: OSD,
        create_link: bool,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<(bool, Option<InventoryItem>), Error> {
        let path = format!("category/{parent_uuid}?tid={}", Self::tid()?);
        match self
            .mutation(
                Self::INVENTORY_CAP_NAME,
                "POST",
                path,
                BTreeMap::new(),
                Some(new_inventory),
                false,
                cancellation_token,
            )
            .await
        {
            Ok((success, _, items, links)) => Ok((
                success,
                if create_link { links } else { items }.into_iter().next(),
            )),
            Err(Error::Cancelled) => Err(Error::Cancelled),
            Err(_) => Ok((false, None)),
        }
    }

    #[allow(dead_code)] // Internal mapped helper used by InventoryManager batch links.
    pub(crate) async fn native_create_inventory_links(
        &self,
        parent_uuid: UUID,
        new_inventory: OSD,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<Vec<InventoryItem>, Error> {
        let Some((response, bytes)) = self
            .request(
                Self::INVENTORY_CAP_NAME,
                "POST",
                format!("category/{parent_uuid}"),
                BTreeMap::new(),
                Some(new_inventory),
                cancellation_token,
            )
            .await?
        else {
            return Ok(Vec::new());
        };
        if !Self::status_ok(&response) {
            return Err(Error::HttpRequest);
        }
        let Some(map) = Self::decode_map(bytes)? else {
            return Ok(Vec::new());
        };
        let (folders, items, links) = self.parse_all_map(&map)?;
        self.reconcile(&map, &folders, &items, &links)?;
        Ok(links)
    }

    pub(crate) async fn native_slam_folder(
        &self,
        folder: UUID,
        payload: OSD,
        token: Option<CancellationToken>,
    ) -> Result<bool, Error> {
        self.mutation_bool(
            Self::INVENTORY_CAP_NAME,
            "PUT",
            format!("category/{folder}/links?tid={}", Self::tid()?),
            BTreeMap::new(),
            Some(payload),
            false,
            token,
        )
        .await
    }
    pub(crate) async fn native_remove_category(
        &self,
        id: UUID,
        token: Option<CancellationToken>,
    ) -> Result<bool, Error> {
        self.mutation_bool(
            Self::INVENTORY_CAP_NAME,
            "DELETE",
            format!("category/{id}"),
            BTreeMap::new(),
            None,
            true,
            token,
        )
        .await
    }
    pub(crate) async fn native_remove_item(
        &self,
        id: UUID,
        token: Option<CancellationToken>,
    ) -> Result<bool, Error> {
        self.mutation_bool(
            Self::INVENTORY_CAP_NAME,
            "DELETE",
            format!("item/{id}"),
            BTreeMap::new(),
            None,
            true,
            token,
        )
        .await
    }
    pub(crate) async fn native_copy_library_category(
        &self,
        source: UUID,
        destination: UUID,
        subfolders: bool,
        token: Option<CancellationToken>,
    ) -> Result<bool, Error> {
        let suffix = if subfolders {
            String::new()
        } else {
            ",depth=0".to_owned()
        };
        self.mutation_bool(
            Self::LIBRARY_CAP_NAME,
            "COPY",
            format!("category/{source}?tid={}{}", Self::tid()?, suffix),
            Self::destination(destination),
            None,
            false,
            token,
        )
        .await
    }
    pub(crate) async fn native_purge_descendents(
        &self,
        id: UUID,
        token: Option<CancellationToken>,
    ) -> Result<bool, Error> {
        self.mutation_bool(
            Self::INVENTORY_CAP_NAME,
            "DELETE",
            format!("category/{id}/children"),
            BTreeMap::new(),
            None,
            false,
            token,
        )
        .await
    }
    pub(crate) async fn native_update_category(
        &self,
        id: UUID,
        payload: OSD,
        token: Option<CancellationToken>,
    ) -> Result<bool, Error> {
        self.mutation_bool(
            Self::INVENTORY_CAP_NAME,
            "PATCH",
            format!("category/{id}"),
            BTreeMap::new(),
            Some(payload),
            false,
            token,
        )
        .await
    }
    pub(crate) async fn native_update_item(
        &self,
        id: UUID,
        payload: OSD,
        token: Option<CancellationToken>,
    ) -> Result<bool, Error> {
        self.mutation_bool(
            Self::INVENTORY_CAP_NAME,
            "PATCH",
            format!("item/{id}"),
            BTreeMap::new(),
            Some(payload),
            false,
            token,
        )
        .await
    }
    pub(crate) async fn native_copy_category(
        &self,
        source: UUID,
        destination: UUID,
        simulate: Option<bool>,
        token: Option<CancellationToken>,
    ) -> Result<bool, Error> {
        let simulate = if simulate.unwrap_or(false) {
            "&simulate=1"
        } else {
            ""
        };
        self.mutation_bool(
            Self::INVENTORY_CAP_NAME,
            "COPY",
            format!("category/{source}?tid={}{}", Self::tid()?, simulate),
            Self::destination(destination),
            None,
            false,
            token,
        )
        .await
    }
    pub(crate) async fn native_move_category(
        &self,
        source: UUID,
        destination: UUID,
        token: Option<CancellationToken>,
    ) -> Result<bool, Error> {
        self.native_update_category(
            source,
            OSD::Map(HashMap::from([(
                "parent_id".into(),
                OSD::UUID(destination),
            )])),
            token,
        )
        .await
    }
    pub(crate) async fn native_put_category_children(
        &self,
        category: String,
        payload: OSD,
        token: Option<CancellationToken>,
    ) -> Result<bool, Error> {
        if !Self::valid_category(&category) {
            return Ok(false);
        }
        self.mutation_bool(
            Self::INVENTORY_CAP_NAME,
            "PUT",
            format!("category/{category}/children?tid={}", Self::tid()?),
            BTreeMap::new(),
            Some(payload),
            false,
            token,
        )
        .await
    }
    pub(crate) async fn native_copy_category_children(
        &self,
        category: String,
        destination: UUID,
        token: Option<CancellationToken>,
    ) -> Result<bool, Error> {
        if !Self::valid_category(&category) {
            return Ok(false);
        }
        self.mutation_bool(
            Self::INVENTORY_CAP_NAME,
            "COPY",
            format!("category/{category}/children?tid={}", Self::tid()?),
            Self::destination(destination),
            None,
            false,
            token,
        )
        .await
    }
    #[allow(clippy::unused_async)] // Fixed Task<bool> API must return an immediately ready future.
    pub(crate) async fn native_move_category_children(
        &self,
        _category: String,
        _destination: UUID,
        _token: Option<CancellationToken>,
    ) -> Result<bool, Error> {
        Ok(false)
    }
    pub(crate) async fn native_delete_category_children(
        &self,
        category: String,
        token: Option<CancellationToken>,
    ) -> Result<bool, Error> {
        if !Self::valid_category(&category) {
            return Ok(false);
        }
        self.mutation_bool(
            Self::INVENTORY_CAP_NAME,
            "DELETE",
            format!("category/{category}/children"),
            BTreeMap::new(),
            None,
            false,
            token,
        )
        .await
    }
    pub(crate) async fn native_put_category_links(
        &self,
        category: String,
        payload: OSD,
        token: Option<CancellationToken>,
    ) -> Result<bool, Error> {
        if !Self::valid_category(&category) {
            return Ok(false);
        }
        self.mutation_bool(
            Self::INVENTORY_CAP_NAME,
            "PUT",
            format!("category/{category}/links?tid={}", Self::tid()?),
            BTreeMap::new(),
            Some(payload),
            false,
            token,
        )
        .await
    }
    pub(crate) async fn native_copy_category_links(
        &self,
        category: String,
        destination: UUID,
        token: Option<CancellationToken>,
    ) -> Result<bool, Error> {
        if !Self::valid_category(&category) {
            return Ok(false);
        }
        self.mutation_bool(
            Self::INVENTORY_CAP_NAME,
            "COPY",
            format!("category/{category}/links?tid={}", Self::tid()?),
            Self::destination(destination),
            None,
            false,
            token,
        )
        .await
    }
    #[allow(clippy::unused_async)] // Fixed Task<bool> API must return an immediately ready future.
    pub(crate) async fn native_move_category_links(
        &self,
        _category: String,
        _destination: UUID,
        _token: Option<CancellationToken>,
    ) -> Result<bool, Error> {
        Ok(false)
    }
    pub(crate) async fn native_delete_category_links(
        &self,
        category: String,
        token: Option<CancellationToken>,
    ) -> Result<bool, Error> {
        if !Self::valid_category(&category) {
            return Ok(false);
        }
        self.mutation_bool(
            Self::INVENTORY_CAP_NAME,
            "DELETE",
            format!("category/{category}/links"),
            BTreeMap::new(),
            None,
            false,
            token,
        )
        .await
    }
    pub(crate) async fn native_copy_item(
        &self,
        item: UUID,
        destination: UUID,
        token: Option<CancellationToken>,
    ) -> Result<bool, Error> {
        self.mutation_bool(
            Self::INVENTORY_CAP_NAME,
            "COPY",
            format!("item/{item}?tid={}", Self::tid()?),
            Self::destination(destination),
            None,
            false,
            token,
        )
        .await
    }
    pub(crate) async fn native_move_item(
        &self,
        item: UUID,
        destination: UUID,
        token: Option<CancellationToken>,
    ) -> Result<bool, Error> {
        self.native_update_item(
            item,
            OSD::Map(HashMap::from([(
                "parent_id".into(),
                OSD::UUID(destination),
            )])),
            token,
        )
        .await
    }

    pub(crate) async fn native_get_category(
        &self,
        category: String,
        token: Option<CancellationToken>,
    ) -> Result<InventoryResult, Error> {
        if !Self::valid_category(&category) {
            return Ok((false, vec![], vec![], vec![]));
        }
        self.get_parsed(
            Self::INVENTORY_CAP_NAME,
            format!("category/{category}"),
            token,
        )
        .await
    }
    pub(crate) async fn native_get_category_children(
        &self,
        category: String,
        depth: i32,
        recursive: bool,
        token: Option<CancellationToken>,
    ) -> Result<InventoryResult, Error> {
        if !Self::valid_category(&category) {
            return Ok((false, vec![], vec![], vec![]));
        }
        let depth = if recursive {
            MAX_FOLDER_DEPTH_REQUEST
        } else {
            depth.min(MAX_FOLDER_DEPTH_REQUEST)
        };
        self.get_parsed(
            Self::INVENTORY_CAP_NAME,
            format!("category/{category}/children?depth={depth}"),
            token,
        )
        .await
    }
    pub(crate) async fn native_fetch_category_categories(
        &self,
        id: UUID,
        inventory: bool,
        recursive: bool,
        depth: Option<i32>,
        token: Option<CancellationToken>,
    ) -> Result<InventoryResult, Error> {
        let depth = if recursive {
            MAX_FOLDER_DEPTH_REQUEST
        } else {
            depth.unwrap_or(0).min(MAX_FOLDER_DEPTH_REQUEST)
        };
        let cap = if inventory {
            Self::INVENTORY_CAP_NAME
        } else {
            Self::LIBRARY_CAP_NAME
        };
        self.get_parsed(
            cap,
            format!("category/{id}/categories?depth={depth}"),
            token,
        )
        .await
    }
    pub(crate) async fn native_fetch_category_subset(
        &self,
        id: UUID,
        children: Box<dyn Iterator<Item = UUID>>,
        inventory: bool,
        recursive: bool,
        depth: Option<i32>,
        token: Option<CancellationToken>,
    ) -> Result<InventoryResult, Error> {
        let children: Vec<_> = children.take(MAX_AIS_OBJECTS).collect();
        if children.is_empty() {
            return Ok((false, vec![], vec![], vec![]));
        }
        let depth = if recursive {
            MAX_FOLDER_DEPTH_REQUEST
        } else {
            depth.unwrap_or(0).min(MAX_FOLDER_DEPTH_REQUEST)
        };
        let list = children
            .iter()
            .map(ToString::to_string)
            .collect::<Vec<_>>()
            .join(",");
        let cap = if inventory {
            Self::INVENTORY_CAP_NAME
        } else {
            Self::LIBRARY_CAP_NAME
        };
        self.get_parsed(
            cap,
            format!("category/{id}/children?depth={depth}&children={list}"),
            token,
        )
        .await
    }
    pub(crate) async fn native_get_category_links(
        &self,
        category: String,
        token: Option<CancellationToken>,
    ) -> Result<InventoryResult, Error> {
        if !Self::valid_category(&category) {
            return Ok((false, vec![], vec![], vec![]));
        }
        self.get_parsed(
            Self::INVENTORY_CAP_NAME,
            format!("category/{category}/links"),
            token,
        )
        .await
    }
    pub(crate) async fn native_fetch_item(
        &self,
        id: UUID,
        token: Option<CancellationToken>,
    ) -> Result<InventoryResult, Error> {
        let result = self
            .get_parsed(
                Self::INVENTORY_CAP_NAME,
                format!("item/{id}"),
                token.clone(),
            )
            .await?;
        if result.0 && result.1.is_empty() && result.2.is_empty() && result.3.is_empty() {
            // `get_parsed` handles embedded shapes; retrying is intentionally avoided.
        }
        Ok(result)
    }
    pub(crate) async fn native_fetch_cof(
        &self,
        token: Option<CancellationToken>,
    ) -> Result<bool, Error> {
        self.native_get_category_links("current".into(), token)
            .await
            .map(|v| v.0)
    }
    pub(crate) async fn native_fetch_orphans(
        &self,
        token: Option<CancellationToken>,
    ) -> Result<bool, Error> {
        self.get_parsed(Self::INVENTORY_CAP_NAME, "orphans".into(), token)
            .await
            .map(|v| v.0)
    }
    pub(crate) async fn native_empty_trash(
        &self,
        token: Option<CancellationToken>,
    ) -> Result<bool, Error> {
        self.native_delete_category_children("trash".into(), token)
            .await
    }

    pub(crate) fn native_parse_items_from_embedded(
        &self,
        response: Option<OSD>,
    ) -> Result<Vec<InventoryItem>, Error> {
        response.map_or(Ok(Vec::new()), |response| self.parse_items_osd(&response))
    }
    pub(crate) fn native_parse_folders_from_embedded(
        &self,
        response: OSDMap,
    ) -> Result<Vec<InventoryFolder>, Error> {
        self.parse_folders_osd(&OSD::Map(response.snapshot()))
    }
    pub(crate) fn native_parse_links_from_embedded(
        &self,
        response: Option<OSD>,
    ) -> Result<Vec<InventoryItem>, Error> {
        response.map_or(Ok(Vec::new()), |response| self.parse_links_osd(&response))
    }
    pub(crate) fn native_parse_embedded(
        &self,
        response: OSD,
        folders: &mut Option<Vec<InventoryFolder>>,
        items: &mut Option<Vec<InventoryItem>>,
        links: &mut Option<Vec<InventoryItem>>,
    ) -> Result<(), Error> {
        let parsed = match response {
            OSD::Map(map) => self.parse_all_map(&map)?,
            _ => return Err(Error::Argument),
        };
        *folders = Some(parsed.0);
        *items = Some(parsed.1);
        *links = Some(parsed.2);
        Ok(())
    }

    fn collection_map<'a>(
        response: &'a OSD,
        plural: &str,
        singular: &str,
    ) -> Option<CollectionSections<'a>> {
        let OSD::Map(root) = response else {
            return None;
        };
        let container = match root.get("_embedded") {
            Some(OSD::Map(map)) => map,
            _ => root,
        };
        let collection = match container.get(plural) {
            Some(OSD::Map(map)) => Some(map),
            _ => None,
        };
        Some((collection, container.get(singular)))
    }

    #[allow(clippy::unused_self)] // Parser belongs to the fixed instance API.
    fn parse_items_osd(&self, response: &OSD) -> Result<Vec<InventoryItem>, Error> {
        let Some((collection, singular)) = Self::collection_map(response, "items", "item") else {
            return Ok(Vec::new());
        };
        let mut result = Vec::new();
        if let Some(collection) = collection {
            for value in collection.values() {
                if let OSD::Map(map) = value {
                    result.push(InventoryItem::from_osd(OSD::Map(map.clone()))?);
                }
            }
        }
        if let Some(OSD::Map(map)) = singular
            && map.contains_key("item_id")
        {
            result.push(InventoryItem::from_osd(OSD::Map(map.clone()))?);
        }
        // GET /item/{id} returns the item itself at the top level.
        if let OSD::Map(map) = response
            && map.contains_key("item_id")
        {
            result.push(InventoryItem::from_osd(response.clone())?);
        }
        Self::dedup_items(result)
    }

    #[allow(clippy::unused_self)] // Parser belongs to the fixed instance API.
    fn parse_folders_osd(&self, response: &OSD) -> Result<Vec<InventoryFolder>, Error> {
        let Some((collection, singular)) = Self::collection_map(response, "categories", "category")
        else {
            return Ok(Vec::new());
        };
        let mut result = Vec::new();
        if let Some(collection) = collection {
            for value in collection.values() {
                if let OSD::Map(map) = value {
                    result.push(InventoryFolder::from_osd(OSD::Map(map.clone()))?);
                }
            }
        }
        if let Some(OSD::Map(map)) = singular
            && map.contains_key("category_id")
        {
            result.push(InventoryFolder::from_osd(OSD::Map(map.clone()))?);
        }
        Self::dedup_folders(result)
    }

    #[allow(clippy::unused_self)] // Parser belongs to the fixed instance API.
    fn parse_links_osd(&self, response: &OSD) -> Result<Vec<InventoryItem>, Error> {
        let Some((collection, _)) = Self::collection_map(response, "links", "link") else {
            return Ok(Vec::new());
        };
        let mut result = Vec::new();
        if let Some(collection) = collection {
            for value in collection.values() {
                let OSD::Map(map) = value else {
                    return Err(Error::Argument);
                };
                let mut item = InventoryItem::from_osd(OSD::Map(map.clone()))?;
                item.set_asset_type(AssetType::Link);
                if let Some(agent) = map.get("agent_id") {
                    let id = agent.as_uuid()?;
                    item.base.set_owner_id(id);
                    item.set_creator_id(id);
                    item.set_last_owner_id(id);
                }
                result.push(item);
            }
        }
        Self::dedup_items(result)
    }

    fn dedup_items(items: Vec<InventoryItem>) -> Result<Vec<InventoryItem>, Error> {
        let mut ids = HashSet::new();
        if items
            .iter()
            .any(|item| item.base.uuid() == UUID::zero() || !ids.insert(item.base.uuid()))
        {
            return Err(Error::Argument);
        }
        Ok(items)
    }
    fn dedup_folders(folders: Vec<InventoryFolder>) -> Result<Vec<InventoryFolder>, Error> {
        let mut ids = HashSet::new();
        if folders
            .iter()
            .any(|folder| folder.base.uuid() == UUID::zero() || !ids.insert(folder.base.uuid()))
        {
            return Err(Error::Argument);
        }
        Ok(folders)
    }

    pub(crate) fn native_parse_ais_response_meta(
        response: Option<OSDMap>,
    ) -> Result<AISResponseMeta, Error> {
        response.map_or_else(
            || Ok(AISResponseMeta::empty()),
            |map| Self::parse_meta_map(&map.snapshot()),
        )
    }
    fn parse_meta_map(map: &HashMap<String, OSD>) -> Result<AISResponseMeta, Error> {
        let broken = Self::uuid_list(map.get("_broken_links_removed"))?;
        let mut removed = Self::uuid_list(map.get("_removed_items"))?;
        removed.extend(Self::uuid_list(map.get("_category_items_removed"))?);
        let categories = Self::uuid_list(map.get("_categories_removed"))?;
        let mut versions = HashMap::new();
        if let Some(OSD::Map(values)) = map.get("_updated_category_versions") {
            for (key, value) in values {
                if let Ok(uuid) = UUID::parse(key.clone())
                    && uuid != UUID::zero()
                {
                    versions.insert(uuid, value.as_integer()?);
                }
            }
        }
        AISResponseMeta::new(broken, removed, categories, versions)
    }
    fn uuid_list(value: Option<&OSD>) -> Result<Vec<UUID>, Error> {
        let mut values = Vec::new();
        match value {
            None => {}
            Some(OSD::Array(array)) => {
                for value in array {
                    let uuid = value.as_uuid()?;
                    if uuid != UUID::zero() {
                        values.push(uuid);
                    }
                }
            }
            Some(OSD::Map(map)) => {
                for key in map.keys() {
                    if let Ok(uuid) = UUID::parse(key.clone())
                        && uuid != UUID::zero()
                    {
                        values.push(uuid);
                    }
                }
            }
            Some(_) => return Err(Error::Argument),
        }
        Ok(values)
    }
}
