/*
 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
 *
 * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
 */

use super::{
    ArchivedResource, DavCollection, DavQuery, DavQueryFilter, ETag, SyncType,
    acl::{DavAclHandler, Privileges},
    lock::{LockData, build_lock_key},
    uri::{UriResource, Urn},
};
use crate::{
    DavError, DavErrorCondition,
    calendar::{
        CALENDAR_CONTAINER_PROPS, CALENDAR_ITEM_PROPS,
        query::{CalendarQueryHandler, try_parse_tz},
    },
    card::{
        CARD_CONTAINER_PROPS, CARD_ITEM_PROPS,
        query::{serialize_vcard_with_props, vcard_query},
    },
    common::{DavQueryResource, acl::current_user_privilege_set, uri::DavUriResource},
    file::{FILE_CONTAINER_PROPS, FILE_ITEM_PROPS},
    principal::{
        CurrentUserPrincipal,
        propfind::{PrincipalPropFind, build_home_set},
    },
};
use calcard::{common::timezone::Tz, icalendar::ICalendarComponentType};
use common::{
    DavResourcePath, DavResources, Server,
    auth::{AccessToken, AccountCache},
};
use dav_proto::{
    Depth, RequestHeaders,
    parser::header::dav_base_uri,
    requests::NsDeadProperty,
    schema::{
        Collation, Namespace,
        property::{
            ActiveLock, CalDavProperty, CardDavProperty, Comp, DavProperty, DavValue,
            PrincipalProperty, Privilege, ReportSet, ResourceType, Rfc1123DateTime,
            SupportedCollation, SupportedLock, WebDavProperty,
        },
        request::{DavDeadProperty, DavPropertyValue, PropFind},
        response::{
            AclRestrictions, BaseCondition, Href, List, MultiStatus, PropStat, Response,
            SupportedPrivilege,
        },
    },
};
use groupware::calendar::{SCHEDULE_INBOX_ID, SupportedComponent};
use groupware::{
    DavCalendarResource, DavResourceName, cache::GroupwareCache, calendar::ArchivedTimezone,
};
use http_proto::HttpResponse;
use hyper::StatusCode;
use registry::schema::{enums::Permission, prelude::ObjectType};
use std::sync::Arc;
use store::{
    ValueKey,
    registry::RegistryQuery,
    write::{AlignedBytes, Archive},
};
use store::{
    ahash::AHashMap,
    query::log::{Change, Query},
    roaring::RoaringBitmap,
};
use trc::AddContext;
use types::{
    acl::Acl,
    collection::{Collection, SyncCollection},
    dead_property::DeadProperty,
};
use utils::map::bitmap::Bitmap;

pub(crate) trait PropFindRequestHandler: Sync + Send {
    fn handle_propfind_request(
        &self,
        access_token: &AccessToken,
        headers: &RequestHeaders<'_>,
        request: PropFind,
    ) -> impl Future<Output = crate::Result<HttpResponse>> + Send;

    fn handle_dav_query(
        &self,
        access_token: &AccessToken,
        query: DavQuery<'_>,
    ) -> impl Future<Output = crate::Result<HttpResponse>> + Send;

    fn dav_quota(
        &self,
        account_id: u32,
    ) -> impl Future<Output = trc::Result<PropFindAccountQuota>> + Send;
}

pub(crate) struct PropFindData {
    pub accounts: AHashMap<u32, PropFindAccountData>,
}

#[derive(Default)]
pub(crate) struct PropFindAccountData {
    pub resources: Option<Arc<DavResources>>,
    pub quota: Option<PropFindAccountQuota>,
    pub owner: Option<Href>,
    pub locks: Option<Archive<AlignedBytes>>,
    pub locks_not_found: bool,
}

#[derive(Clone, Default)]
pub(crate) struct PropFindAccountQuota {
    pub used: u64,
    pub available: u64,
}

#[derive(Debug)]
pub(crate) struct PropFindItem {
    pub name: String,
    pub account_id: u32,
    pub document_id: u32,
    pub parent_id: Option<u32>,
    pub is_container: bool,
}

impl PropFindRequestHandler for Server {
    async fn handle_propfind_request(
        &self,
        access_token: &AccessToken,
        headers: &RequestHeaders<'_>,
        request: PropFind,
    ) -> crate::Result<HttpResponse> {
        // Validate URI
        let resource = self.validate_uri(access_token, headers.uri).await?;

        // Reject Infinity depth for certain queries
        let return_children = match headers.depth {
            Depth::One | Depth::None => true,
            Depth::Zero => false,
            Depth::Infinity => match resource.collection {
                Collection::Principal => true,
                Collection::Calendar | Collection::AddressBook
                    if resource.account_id.is_some() && resource.resource.is_some() =>
                {
                    true
                }
                Collection::CalendarEventNotification if resource.account_id.is_some() => true,
                _ => {
                    return Err(DavErrorCondition::new(
                        StatusCode::FORBIDDEN,
                        BaseCondition::PropFindFiniteDepth,
                    )
                    .into());
                }
            },
        };

        // List shared resources
        if let Some(account_id) = resource.account_id {
            match resource.collection {
                Collection::FileNode
                | Collection::Calendar
                | Collection::AddressBook
                | Collection::CalendarEventNotification => {
                    // Validate permissions
                    access_token.enforce_permission(match resource.collection {
                        Collection::FileNode => Permission::DavFilePropFind,
                        Collection::Calendar
                        | Collection::CalendarEvent
                        | Collection::CalendarEventNotification => Permission::DavCalPropFind,
                        Collection::AddressBook | Collection::ContactCard => {
                            Permission::DavCardPropFind
                        }
                        _ => unreachable!(),
                    })?;

                    self.handle_dav_query(
                        access_token,
                        DavQuery::propfind(
                            UriResource::new_owned(
                                resource.collection,
                                account_id,
                                resource.resource,
                            ),
                            request,
                            headers,
                        ),
                    )
                    .await
                }
                Collection::Principal => {
                    let mut response = MultiStatus::new(Vec::with_capacity(16));

                    if resource.resource.is_some() {
                        response.add_response(Response::new_status(
                            [headers.uri.to_string()],
                            StatusCode::NOT_FOUND,
                        ));
                    } else if access_token.has_account_access(account_id)
                        || (self.core.groupware.allow_directory_query
                            && access_token.has_permission(Permission::DavPrincipalList))
                        || access_token.has_permission(Permission::SysAccountQuery)
                    {
                        self.prepare_principal_propfind_response(
                            access_token,
                            Collection::Principal,
                            [account_id].into_iter(),
                            &request,
                            &mut response,
                        )
                        .await?;
                    } else {
                        response.add_response(Response::new_status(
                            [headers.uri.to_string()],
                            StatusCode::FORBIDDEN,
                        ));
                    }

                    Ok(HttpResponse::new(StatusCode::MULTI_STATUS)
                        .with_xml_body(response.to_string()))
                }
                _ => unreachable!(),
            }
        } else {
            let mut response = MultiStatus::new(Vec::with_capacity(16));

            // Add container info
            if !headers.depth_no_root {
                add_base_collection_response(
                    self,
                    &request,
                    resource.collection,
                    access_token,
                    &mut response,
                )
                .await?;
            }

            if return_children {
                let ids = if !matches!(resource.collection, Collection::Principal) {
                    // Validate permissions
                    access_token.enforce_permission(match resource.collection {
                        Collection::FileNode => Permission::DavFilePropFind,
                        Collection::Calendar
                        | Collection::CalendarEvent
                        | Collection::CalendarEventNotification => Permission::DavCalPropFind,
                        Collection::AddressBook | Collection::ContactCard => {
                            Permission::DavCardPropFind
                        }
                        _ => unreachable!(),
                    })?;
                    RoaringBitmap::from_iter(
                        access_token.all_ids_by_collection(resource.collection),
                    )
                } else if (self.core.groupware.allow_directory_query
                    && access_token.has_permission(Permission::DavPrincipalList))
                    || access_token.has_permission(Permission::SysAccountQuery)
                {
                    // Return all principals
                    self.registry()
                        .query::<RoaringBitmap>(
                            RegistryQuery::new(ObjectType::Account)
                                .with_tenant(access_token.tenant_id()),
                        )
                        .await
                        .caused_by(trc::location!())?
                } else {
                    RoaringBitmap::from_iter(access_token.all_ids())
                };

                self.prepare_principal_propfind_response(
                    access_token,
                    resource.collection,
                    ids.into_iter(),
                    &request,
                    &mut response,
                )
                .await?;
            }

            Ok(HttpResponse::new(StatusCode::MULTI_STATUS).with_xml_body(response.to_string()))
        }
    }

    async fn handle_dav_query(
        &self,
        access_token: &AccessToken,
        mut query: DavQuery<'_>,
    ) -> crate::Result<HttpResponse> {
        let mut response = MultiStatus::new(Vec::with_capacity(16));
        let mut data = PropFindData::new();
        let collection_container;
        let collection_children;
        let sync_collection;
        let mut query_filter = None;
        let mut limit = std::cmp::min(
            query.limit.unwrap_or(u32::MAX) as usize,
            self.core.groupware.max_results,
        );
        let mut is_sync_limited = false;
        let mut is_propfind = false;
        let mut ical_instances_limit = self.core.groupware.max_ical_instances;

        let paths = match std::mem::take(&mut query.resource) {
            DavQueryResource::Uri(resource) => {
                collection_container = resource.collection;
                collection_children = collection_container.child_collection().unwrap();
                sync_collection = SyncCollection::from(collection_container);
                is_propfind = true;

                get(
                    self,
                    access_token,
                    collection_container,
                    collection_children,
                    sync_collection,
                    &query,
                    &mut data,
                    &mut response,
                    resource,
                    limit,
                    &mut is_sync_limited,
                )
                .await?
            }
            DavQueryResource::Multiget {
                hrefs,
                parent_collection,
            } => {
                collection_container = parent_collection;
                collection_children = collection_container.child_collection().unwrap();
                sync_collection = SyncCollection::from(collection_container);

                multiget(
                    self,
                    access_token,
                    collection_container,
                    collection_children,
                    sync_collection,
                    &mut data,
                    &mut response,
                    hrefs,
                )
                .await?
            }
            DavQueryResource::Query {
                filter,
                parent_collection,
                items,
            } => {
                query_filter = Some(filter);
                collection_container = parent_collection;
                collection_children = collection_container.child_collection().unwrap();
                sync_collection = SyncCollection::from(collection_container);

                items
            }
            DavQueryResource::None => unreachable!(),
        };
        response.set_namespace(collection_container.namespace());

        let mut skip_not_found = query.expand;
        let properties = match &query.propfind {
            PropFind::PropName => {
                let (container_props, children_props) = match collection_container {
                    Collection::FileNode => {
                        (FILE_CONTAINER_PROPS.as_slice(), FILE_ITEM_PROPS.as_slice())
                    }
                    Collection::Calendar | Collection::CalendarEventNotification => (
                        CALENDAR_CONTAINER_PROPS.as_slice(),
                        CALENDAR_ITEM_PROPS.as_slice(),
                    ),
                    Collection::AddressBook => {
                        (CARD_CONTAINER_PROPS.as_slice(), CARD_ITEM_PROPS.as_slice())
                    }
                    _ => unreachable!(),
                };

                for item in paths {
                    let props = if item.is_container {
                        container_props
                            .iter()
                            .cloned()
                            .map(DavPropertyValue::empty)
                            .collect::<Vec<_>>()
                    } else {
                        children_props
                            .iter()
                            .cloned()
                            .map(DavPropertyValue::empty)
                            .collect::<Vec<_>>()
                    };

                    response.add_response(Response::new_propstat(
                        item.name,
                        vec![PropStat::new_list(props)],
                    ));
                }

                return Ok(
                    HttpResponse::new(StatusCode::MULTI_STATUS).with_xml_body(response.to_string())
                );
            }
            PropFind::AllProp(items) => {
                skip_not_found = true;
                let mut result = Vec::with_capacity(items.len() + DavProperty::ALL_PROPS.len());
                result.extend(DavProperty::ALL_PROPS);
                result.extend(items.iter().filter(|field| !field.is_all_prop()).cloned());
                result
            }
            PropFind::Prop(items) => items.clone(),
        };

        let is_scheduling = collection_container == Collection::CalendarEventNotification;
        let account_info = self
            .account(access_token.account_id())
            .await
            .caused_by(trc::location!())?;
        'outer: for item in paths {
            let account_id = item.account_id;
            let document_id = item.document_id;
            let collection = if item.is_container {
                collection_container
            } else {
                collection_children
            };

            // Unarchive resource
            let archive_;
            let archive = if is_scheduling && item.is_container {
                archive_ = Archive::default();
                ArchivedResource::CalendarEventNotificationCollection(
                    item.document_id == SCHEDULE_INBOX_ID,
                )
            } else if let Some(archive) = self
                .store()
                .get_value::<Archive<AlignedBytes>>(ValueKey::archive(
                    account_id,
                    collection,
                    document_id,
                ))
                .await
                .caused_by(trc::location!())?
            {
                archive_ = archive;
                ArchivedResource::from_archive(&archive_, collection).caused_by(trc::location!())?
            } else {
                response.add_response(Response::new_status([item.name], StatusCode::NOT_FOUND));
                continue;
            };

            // Filter
            let mut calendar_filter = None;
            if let Some(query_filter) = &query_filter {
                match (query_filter, &archive) {
                    (DavQueryFilter::Addressbook(filter), ArchivedResource::ContactCard(card))
                        if !vcard_query(&card.inner.card, filter) =>
                    {
                        continue;
                    }
                    (
                        DavQueryFilter::Calendar {
                            filter,
                            timezone,
                            max_time_range,
                        },
                        ArchivedResource::CalendarEvent(event),
                    ) => {
                        let default_tz = if let Some(tz) = try_parse_tz(timezone) {
                            tz
                        } else if let Some(calendar_id) = item.parent_id {
                            data.resources(self, access_token, account_id, SyncCollection::Calendar)
                                .await
                                .caused_by(trc::location!())?
                                .calendar_default_tz(calendar_id, account_id)
                                .unwrap_or(Tz::UTC)
                        } else {
                            Tz::UTC
                        };
                        let mut query_handler =
                            CalendarQueryHandler::new(event.inner, *max_time_range, default_tz);
                        if !query_handler.filter(event.inner, filter) {
                            continue;
                        }
                        calendar_filter = Some(query_handler);
                    }
                    _ => (),
                }
            }

            // Fill properties
            let dead_properties = archive.dead_properties();
            let mut fields = Vec::with_capacity(properties.len());
            let mut fields_not_found = Vec::new();
            for property in &properties {
                match property {
                    DavProperty::WebDav(dav_property) => match dav_property {
                        WebDavProperty::CreationDate => {
                            fields.push(DavPropertyValue::new(
                                property.clone(),
                                DavValue::Timestamp(archive.created()),
                            ));
                        }
                        WebDavProperty::DisplayName => {
                            if let Some(name) = archive.display_name(access_token) {
                                fields.push(DavPropertyValue::new(
                                    property.clone(),
                                    DavValue::String(name.to_string()),
                                ));
                            } else if !skip_not_found {
                                fields_not_found.push(DavPropertyValue::empty(property.clone()));
                            }
                        }
                        WebDavProperty::GetContentLanguage => {
                            if !skip_not_found {
                                fields_not_found.push(DavPropertyValue::empty(property.clone()));
                            }
                        }
                        WebDavProperty::GetContentLength => {
                            if let Some(value) = archive.content_length() {
                                fields.push(DavPropertyValue::new(
                                    property.clone(),
                                    DavValue::Uint64(value as u64),
                                ));
                            } else if !skip_not_found {
                                fields_not_found.push(DavPropertyValue::empty(property.clone()));
                            }
                        }
                        WebDavProperty::GetContentType => {
                            if let Some(value) = archive.content_type() {
                                fields.push(DavPropertyValue::new(
                                    property.clone(),
                                    DavValue::String(value.to_string()),
                                ));
                            } else if !skip_not_found {
                                fields_not_found.push(DavPropertyValue::empty(property.clone()));
                            }
                        }
                        WebDavProperty::GetETag => {
                            fields.push(DavPropertyValue::new(
                                property.clone(),
                                DavValue::String(archive_.etag()),
                            ));
                        }
                        WebDavProperty::GetCTag => {
                            if item.is_container {
                                let ctag = data
                                    .resources(self, access_token, account_id, sync_collection)
                                    .await
                                    .caused_by(trc::location!())?
                                    .highest_change_id;

                                fields.push(DavPropertyValue::new(
                                    property.clone(),
                                    DavValue::String(format!("\"{ctag}\"")),
                                ));
                            } else {
                                fields_not_found.push(DavPropertyValue::empty(property.clone()));
                            }
                            response.set_namespace(Namespace::CalendarServer);
                        }
                        WebDavProperty::GetLastModified => {
                            fields.push(DavPropertyValue::new(
                                property.clone(),
                                DavValue::Rfc1123Date(Rfc1123DateTime::new(archive.modified())),
                            ));
                        }
                        WebDavProperty::ResourceType => {
                            if let Some(resource_type) = archive.resource_type() {
                                fields.push(DavPropertyValue::new(property.clone(), resource_type));
                            } else {
                                fields.push(DavPropertyValue::empty(property.clone()));
                            }
                        }
                        WebDavProperty::LockDiscovery => {
                            if let Some(locks) = data
                                .locks(self, account_id, collection_container, &item)
                                .await
                                .caused_by(trc::location!())?
                            {
                                fields.push(DavPropertyValue::new(property.clone(), locks));
                            } else {
                                fields.push(DavPropertyValue::empty(property.clone()));
                            }
                        }
                        WebDavProperty::SupportedLock => {
                            if !is_scheduling {
                                fields.push(DavPropertyValue::new(
                                    property.clone(),
                                    SupportedLock::default(),
                                ));
                            } else {
                                fields.push(DavPropertyValue::empty(property.clone()));
                            }
                        }
                        WebDavProperty::SupportedReportSet => {
                            if let Some(report_set) = archive.supported_report_set() {
                                fields.push(DavPropertyValue::new(property.clone(), report_set));
                            } else if !skip_not_found {
                                fields_not_found.push(DavPropertyValue::empty(property.clone()));
                            }
                        }
                        WebDavProperty::SyncToken => {
                            let sync_token = data
                                .resources(self, access_token, account_id, sync_collection)
                                .await
                                .caused_by(trc::location!())?
                                .sync_token();

                            fields.push(DavPropertyValue::new(property.clone(), sync_token));
                        }
                        WebDavProperty::CurrentUserPrincipal => {
                            if !query.expand {
                                fields.push(DavPropertyValue::new(
                                    property.clone(),
                                    vec![account_info.current_user_principal()],
                                ));
                            } else {
                                fields.push(DavPropertyValue::new(
                                    property.clone(),
                                    self.expand_principal(
                                        access_token,
                                        access_token.account_id(),
                                        &query.propfind,
                                    )
                                    .await?
                                    .map(|r| DavValue::Response(Box::new(r)))
                                    .unwrap_or(DavValue::Null),
                                ));
                            }
                        }
                        WebDavProperty::QuotaAvailableBytes => {
                            if item.is_container {
                                fields.push(DavPropertyValue::new(
                                    property.clone(),
                                    data.quota(self, account_id)
                                        .await
                                        .caused_by(trc::location!())?
                                        .available,
                                ));
                            } else if !skip_not_found {
                                fields_not_found.push(DavPropertyValue::empty(property.clone()));
                            }
                        }
                        WebDavProperty::QuotaUsedBytes => {
                            if item.is_container {
                                fields.push(DavPropertyValue::new(
                                    property.clone(),
                                    data.quota(self, account_id)
                                        .await
                                        .caused_by(trc::location!())?
                                        .used,
                                ));
                            } else if !skip_not_found {
                                fields_not_found.push(DavPropertyValue::empty(property.clone()));
                            }
                        }
                        WebDavProperty::Owner => {
                            if !query.expand {
                                fields.push(DavPropertyValue::new(
                                    property.clone(),
                                    vec![
                                        data.owner(self, &account_info, account_id)
                                            .await
                                            .caused_by(trc::location!())?,
                                    ],
                                ));
                            } else {
                                fields.push(DavPropertyValue::new(
                                    property.clone(),
                                    self.expand_principal(
                                        access_token,
                                        account_id,
                                        &query.propfind,
                                    )
                                    .await?
                                    .map(|r| DavValue::Response(Box::new(r)))
                                    .unwrap_or(DavValue::Null),
                                ));
                            }
                        }
                        WebDavProperty::Group => {
                            fields.push(DavPropertyValue::empty(property.clone()));
                        }
                        WebDavProperty::SupportedPrivilegeSet => {
                            if !is_scheduling {
                                fields.push(DavPropertyValue::new(
                                    property.clone(),
                                    vec![SupportedPrivilege::all_privileges(
                                        collection_container == Collection::Calendar,
                                    )],
                                ));
                            } else {
                                fields.push(DavPropertyValue::new(
                                    property.clone(),
                                    vec![SupportedPrivilege::all_scheduling_privileges(matches!(
                                        archive,
                                        ArchivedResource::CalendarEventNotification(_)
                                            | ArchivedResource::CalendarEventNotificationCollection(
                                                true
                                            )
                                    ))],
                                ));
                            }
                        }
                        WebDavProperty::CurrentUserPrivilegeSet => {
                            let privileges = if is_scheduling {
                                Privilege::scheduling(
                                    matches!(
                                        archive,
                                        ArchivedResource::CalendarEventNotification(_)
                                            | ArchivedResource::CalendarEventNotificationCollection(
                                                true
                                            )
                                    ),
                                    access_token.is_member(account_id),
                                )
                            } else if access_token.is_member(account_id) {
                                Privilege::all(matches!(
                                    collection,
                                    Collection::Calendar | Collection::CalendarEvent
                                ))
                            } else if let Some(acls) = archive.acls() {
                                access_token.current_privilege_set(
                                    account_id,
                                    acls,
                                    collection_container == Collection::Calendar,
                                )
                            } else if let Some(parent_id) = item.parent_id {
                                current_user_privilege_set(
                                    data.resources(self, access_token, account_id, sync_collection)
                                        .await
                                        .caused_by(trc::location!())?
                                        .container_acl(access_token, parent_id),
                                )
                            } else {
                                vec![]
                            };

                            if !privileges.is_empty() {
                                fields.push(DavPropertyValue::new(property.clone(), privileges));
                            } else if !skip_not_found {
                                fields_not_found.push(DavPropertyValue::empty(property.clone()));
                            }
                        }
                        WebDavProperty::Acl => {
                            if let Some(acls) = archive.acls() {
                                let aces = self
                                    .resolve_ace(
                                        access_token,
                                        account_id,
                                        acls,
                                        query.expand.then_some(&query.propfind),
                                    )
                                    .await?;

                                fields.push(DavPropertyValue::new(property.clone(), aces));
                            } else if !skip_not_found {
                                fields_not_found.push(DavPropertyValue::empty(property.clone()));
                            }
                        }
                        WebDavProperty::AclRestrictions => {
                            fields.push(DavPropertyValue::new(
                                property.clone(),
                                AclRestrictions::default()
                                    .with_no_invert()
                                    .with_grant_only(),
                            ));
                        }
                        WebDavProperty::InheritedAclSet => {
                            fields.push(DavPropertyValue::empty(property.clone()));
                        }
                        WebDavProperty::PrincipalCollectionSet => {
                            fields.push(DavPropertyValue::new(
                                property.clone(),
                                vec![Href(
                                    DavResourceName::Principal.collection_path().to_string(),
                                )],
                            ));
                        }
                    },
                    DavProperty::DeadProperty(tag) => {
                        if let Some(value) =
                            dead_properties.and_then(|props| props.find_tag(&tag.name))
                        {
                            fields.push(DavPropertyValue::new(property.clone(), value));
                        } else {
                            fields_not_found.push(DavPropertyValue::empty(property.clone()));
                        }
                    }
                    DavProperty::CardDav(card_property) => match (card_property, &archive) {
                        (
                            CardDavProperty::AddressbookDescription,
                            ArchivedResource::AddressBook(book),
                        ) => {
                            if let Some(desc) =
                                book.inner.preferences(access_token).description.as_deref()
                            {
                                fields.push(DavPropertyValue::new(
                                    property.clone(),
                                    desc.to_string(),
                                ));
                            } else {
                                fields_not_found.push(DavPropertyValue::empty(property.clone()));
                            }
                        }
                        (
                            CardDavProperty::SupportedAddressData,
                            ArchivedResource::AddressBook(_),
                        ) => {
                            fields.push(DavPropertyValue::new(
                                property.clone(),
                                DavValue::SupportedAddressData,
                            ));
                        }
                        (
                            CardDavProperty::SupportedCollationSet,
                            ArchivedResource::AddressBook(_),
                        ) => {
                            fields.push(DavPropertyValue::new(
                                property.clone(),
                                DavValue::Collations(List(vec![
                                    SupportedCollation {
                                        collation: Collation::AsciiCasemap,
                                        namespace: Namespace::CardDav,
                                    },
                                    SupportedCollation {
                                        collation: Collation::UnicodeCasemap,
                                        namespace: Namespace::CardDav,
                                    },
                                ])),
                            ));
                        }
                        (CardDavProperty::MaxResourceSize, ArchivedResource::AddressBook(_)) => {
                            fields.push(DavPropertyValue::new(
                                property.clone(),
                                self.core.groupware.max_vcard_size as u64,
                            ));
                        }
                        (
                            CardDavProperty::AddressData {
                                properties,
                                version,
                            },
                            ArchivedResource::ContactCard(card),
                        ) => {
                            fields.push(DavPropertyValue::new(
                                property.clone(),
                                DavValue::CData(serialize_vcard_with_props(
                                    &card.inner.card,
                                    properties,
                                    (*version)
                                        .or(query.max_vcard_version)
                                        .or_else(|| card.inner.card.version()),
                                )),
                            ));
                        }
                        _ => {
                            if !skip_not_found {
                                fields_not_found.push(DavPropertyValue::empty(property.clone()));
                            }
                        }
                    },
                    DavProperty::CalDav(cal_property) => match (cal_property, &archive) {
                        (
                            CalDavProperty::CalendarDescription,
                            ArchivedResource::Calendar(calendar),
                        ) => {
                            if let Some(desc) = calendar
                                .inner
                                .preferences(access_token)
                                .description
                                .as_deref()
                            {
                                fields.push(DavPropertyValue::new(
                                    property.clone(),
                                    desc.to_string(),
                                ));
                            } else {
                                fields_not_found.push(DavPropertyValue::empty(property.clone()));
                            }
                        }
                        (
                            CalDavProperty::CalendarTimezone,
                            ArchivedResource::Calendar(calendar),
                        ) => {
                            if let ArchivedTimezone::Custom(tz) =
                                &calendar.inner.preferences(access_token).time_zone
                            {
                                fields.push(DavPropertyValue::new(
                                    property.clone(),
                                    DavValue::CData(tz.to_string()),
                                ));
                            } else {
                                fields_not_found.push(DavPropertyValue::empty(property.clone()));
                            }
                        }
                        (CalDavProperty::TimezoneId, ArchivedResource::Calendar(calendar)) => {
                            if let ArchivedTimezone::IANA(tz) =
                                &calendar.inner.preferences(access_token).time_zone
                            {
                                fields.push(DavPropertyValue::new(
                                    property.clone(),
                                    Tz::from_id(tz.to_native()).unwrap_or(Tz::UTC).to_string(),
                                ));
                            } else {
                                fields_not_found.push(DavPropertyValue::empty(property.clone()));
                            }
                        }
                        (
                            CalDavProperty::SupportedCalendarComponentSet,
                            ArchivedResource::Calendar(calendar),
                        ) => {
                            let supported_components =
                                calendar.inner.supported_components.to_native();
                            fields.push(DavPropertyValue::new(
                                property.clone(),
                                if supported_components != 0 {
                                    DavValue::Components(List(
                                        Bitmap::<SupportedComponent>::from(supported_components)
                                            .into_iter()
                                            .map(ICalendarComponentType::from)
                                            .map(Comp)
                                            .collect(),
                                    ))
                                } else {
                                    DavValue::all_calendar_components()
                                },
                            ));
                        }
                        (CalDavProperty::SupportedCalendarData, ArchivedResource::Calendar(_)) => {
                            fields.push(DavPropertyValue::new(
                                property.clone(),
                                DavValue::SupportedCalendarData,
                            ));
                        }
                        (CalDavProperty::SupportedCollationSet, ArchivedResource::Calendar(_)) => {
                            fields.push(DavPropertyValue::new(
                                property.clone(),
                                DavValue::Collations(List(vec![
                                    SupportedCollation {
                                        collation: Collation::AsciiCasemap,
                                        namespace: Namespace::CalDav,
                                    },
                                    SupportedCollation {
                                        collation: Collation::UnicodeCasemap,
                                        namespace: Namespace::CalDav,
                                    },
                                ])),
                            ));
                        }
                        (CalDavProperty::MaxResourceSize, ArchivedResource::Calendar(_)) => {
                            fields.push(DavPropertyValue::new(
                                property.clone(),
                                self.core.groupware.max_ical_size as u64,
                            ));
                        }
                        (CalDavProperty::MinDateTime, ArchivedResource::Calendar(_)) => {
                            fields.push(DavPropertyValue::new(
                                property.clone(),
                                DavValue::String("0001-01-01T00:00:00Z".to_string()),
                            ));
                        }
                        (CalDavProperty::MaxDateTime, ArchivedResource::Calendar(_)) => {
                            fields.push(DavPropertyValue::new(
                                property.clone(),
                                DavValue::String("9999-12-31T23:59:59Z".to_string()),
                            ));
                        }
                        (CalDavProperty::MaxInstances, ArchivedResource::Calendar(_)) => {
                            fields.push(DavPropertyValue::new(
                                property.clone(),
                                self.core.groupware.max_ical_instances as u64,
                            ));
                        }
                        (
                            CalDavProperty::MaxAttendeesPerInstance,
                            ArchivedResource::Calendar(_),
                        ) => {
                            fields.push(DavPropertyValue::new(
                                property.clone(),
                                self.core.groupware.max_ical_attendees_per_instance as u64,
                            ));
                        }
                        (
                            CalDavProperty::CalendarData(data),
                            ArchivedResource::CalendarEvent(event),
                        ) => {
                            if calendar_filter.is_some() || !data.properties.is_empty() {
                                if let Some(ical) = calendar_filter
                                    .get_or_insert_with(|| {
                                        CalendarQueryHandler::new(event.inner, None, Tz::UTC)
                                    })
                                    .serialize_ical(event.inner, data, &mut ical_instances_limit)
                                {
                                    fields.push(DavPropertyValue::new(
                                        property.clone(),
                                        DavValue::CData(ical),
                                    ));
                                } else {
                                    limit = 0;
                                    break 'outer;
                                }
                            } else {
                                fields.push(DavPropertyValue::new(
                                    property.clone(),
                                    DavValue::CData(event.inner.data.event.to_string()),
                                ));
                            }
                        }
                        (
                            CalDavProperty::CalendarData(_),
                            ArchivedResource::CalendarEventNotification(event),
                        ) => {
                            fields.push(DavPropertyValue::new(
                                property.clone(),
                                DavValue::CData(event.inner.event.to_string()),
                            ));
                        }
                        (CalDavProperty::ScheduleTag, ArchivedResource::CalendarEvent(event))
                            if event.inner.schedule_tag.is_some() =>
                        {
                            fields.push(DavPropertyValue::new(
                                property.clone(),
                                DavValue::String(format!(
                                    "\"{}\"",
                                    event.inner.schedule_tag.as_ref().unwrap()
                                )),
                            ));
                        }
                        (CalDavProperty::ScheduleCalendarTransp, ArchivedResource::Calendar(_)) => {
                            fields.push(DavPropertyValue::new(
                                property.clone(),
                                DavValue::DeadProperty(DeadProperty::single_with_ns(
                                    Namespace::CalDav,
                                    "opaque",
                                )),
                            ));
                        }
                        (
                            CalDavProperty::ScheduleDefaultCalendarURL,
                            ArchivedResource::CalendarEventNotificationCollection(true),
                        ) => {
                            if let Some(default_cal) = &self.core.groupware.default_calendar_name {
                                fields.push(DavPropertyValue::new(
                                    property.clone(),
                                    vec![Href(format!(
                                        "{}/{}/{default_cal}/",
                                        DavResourceName::Cal.base_path(),
                                        item.name.split('/').nth(3).unwrap_or_default()
                                    ))],
                                ));
                            } else {
                                fields_not_found.push(DavPropertyValue::empty(property.clone()));
                            }
                        }

                        _ => {
                            if !skip_not_found {
                                response.set_namespace(property.namespace());
                                fields_not_found.push(DavPropertyValue::empty(property.clone()));
                            }
                        }
                    },

                    property => {
                        if !skip_not_found {
                            response.set_namespace(property.namespace());
                            fields_not_found.push(DavPropertyValue::empty(property.clone()));
                        }
                    }
                }
            }

            // Add dead properties
            if skip_not_found
                && let Some(dead_properties) =
                    dead_properties.filter(|dead_properties| !dead_properties.0.is_empty())
            {
                dead_properties.to_dav_values(&mut fields);
            }

            // Add response
            let mut prop_stat = Vec::with_capacity(2);
            if !fields.is_empty() {
                prop_stat.push(PropStat::new_list(fields));
            }
            if !fields_not_found.is_empty() && !query.is_minimal() {
                prop_stat
                    .push(PropStat::new_list(fields_not_found).with_status(StatusCode::NOT_FOUND));
            }
            if prop_stat.is_empty() {
                prop_stat.push(PropStat::new_list(vec![]));
            }
            response.add_response(Response::new_propstat(item.name, prop_stat));

            limit -= 1;
            if limit == 0 {
                break;
            }
        }

        if limit == 0 || is_sync_limited {
            response.add_response(
                Response::new_status([query.uri], StatusCode::INSUFFICIENT_STORAGE)
                    .with_error(BaseCondition::NumberOfMatchesWithinLimit)
                    .with_response_description(if ical_instances_limit > 0 {
                        format!(
                            "The number of matches exceeds the limit of {}",
                            query
                                .limit
                                .unwrap_or(self.core.groupware.max_results as u32)
                        )
                    } else {
                        format!(
                            "The number of recurrence instances exceeds the limit of {}",
                            query
                                .limit
                                .unwrap_or(self.core.groupware.max_ical_instances as u32)
                        )
                    }),
            );
        }

        if !response.response.0.is_empty() || !query.sync_type.is_none() {
            Ok(HttpResponse::new(StatusCode::MULTI_STATUS).with_xml_body(response.to_string()))
        } else if !is_propfind {
            Ok(HttpResponse::new(StatusCode::MULTI_STATUS)
                .with_xml_body(MultiStatus::not_found(query.uri).to_string()))
        } else {
            Ok(HttpResponse::new(StatusCode::NOT_FOUND))
        }
    }

    async fn dav_quota(&self, account_id: u32) -> trc::Result<PropFindAccountQuota> {
        let account = self.account(account_id).await.caused_by(trc::location!())?;
        let quota = if account.quota_disk > 0 {
            account.quota_disk
        } else if let Some(tenant_id) = account.id_tenant {
            let tenant = self.tenant(tenant_id).await.caused_by(trc::location!())?;
            if tenant.quota_disk > 0 {
                tenant.quota_disk
            } else {
                u32::MAX as u64
            }
        } else {
            u32::MAX as u64
        };
        let used = self
            .get_used_quota_account(account_id)
            .await
            .caused_by(trc::location!())? as u64;

        Ok(PropFindAccountQuota {
            used,
            available: quota.saturating_sub(used),
        })
    }
}
#[allow(clippy::too_many_arguments)]
async fn get(
    server: &Server,
    access_token: &AccessToken,
    collection_container: Collection,
    collection_children: Collection,
    sync_collection: SyncCollection,
    query: &DavQuery<'_>,
    data: &mut PropFindData,
    response: &mut MultiStatus,
    resource: UriResource<u32, Option<&str>>,
    limit: usize,
    is_sync_limited: &mut bool,
) -> crate::Result<Vec<PropFindItem>> {
    let container_has_children = collection_children != collection_container;
    response.set_namespace(collection_container.namespace());

    let account_id = resource.account_id;
    let resources = data
        .resources(server, access_token, account_id, sync_collection)
        .await
        .caused_by(trc::location!())?;

    // Obtain document ids
    let mut display_containers = if !access_token.is_member(account_id) {
        resources
            .shared_containers(
                access_token,
                [if container_has_children {
                    Acl::ReadItems
                } else {
                    Acl::Read
                }],
                true,
            )
            .into()
    } else {
        None
    };
    let mut display_children = display_containers
        .as_ref()
        .filter(|_| container_has_children)
        .map(|containers| {
            RoaringBitmap::from_iter(resources.resources.iter().filter_map(|r| {
                if r.child_names()
                    .is_some_and(|n| n.iter().any(|n| containers.contains(n.parent_id)))
                {
                    Some(r.document_id)
                } else {
                    None
                }
            }))
        });

    // Filter by changelog
    let is_sync = match query.sync_type {
        SyncType::From { id, seq } => {
            let changes = server
                .store()
                .changes(account_id, sync_collection.into(), Query::Since(id))
                .await
                .caused_by(trc::location!())?;
            let mut vanished: Vec<String> = Vec::new();

            // Merge changes
            let mut total_changes = 0;
            let mut maybe_has_vanished = false;
            if container_has_children {
                let mut container_changes = RoaringBitmap::new();
                let mut item_changes = RoaringBitmap::new();

                for change in changes.changes {
                    match change {
                        Change::InsertItem(id) => {
                            item_changes.insert(id as u32);
                        }
                        Change::UpdateItem(id) => {
                            maybe_has_vanished = true;
                            item_changes.insert(id as u32);
                        }
                        Change::InsertContainer(id) => {
                            container_changes.insert(id as u32);
                        }
                        Change::UpdateContainer(id) => {
                            maybe_has_vanished = true;
                            container_changes.insert(id as u32);
                        }
                        Change::DeleteContainer(_) | Change::DeleteItem(_) => {
                            maybe_has_vanished = true;
                        }
                        Change::UpdateContainerProperty(_) => (),
                    }
                }

                for (document_ids, changes) in [
                    (&mut display_containers, container_changes),
                    (&mut display_children, item_changes),
                ] {
                    if let Some(document_ids) = document_ids {
                        *document_ids &= changes;
                        total_changes += document_ids.len() as usize;
                    } else {
                        total_changes += changes.len() as usize;
                        *document_ids = Some(changes);
                    }
                }
            } else {
                let changes = RoaringBitmap::from_iter(changes.changes.iter().filter_map(
                    |change| match change {
                        Change::InsertItem(id) | Change::InsertContainer(id) => Some(*id as u32),
                        Change::UpdateItem(id) | Change::UpdateContainer(id) => {
                            maybe_has_vanished = true;
                            Some(*id as u32)
                        }
                        Change::DeleteContainer(_) | Change::DeleteItem(_) => {
                            maybe_has_vanished = true;
                            None
                        }
                        _ => None,
                    },
                ));
                if let Some(document_ids) = &mut display_containers {
                    *document_ids &= changes;
                    total_changes += document_ids.len() as usize;
                } else {
                    total_changes += changes.len() as usize;
                    display_containers = Some(changes);
                }
            }

            if maybe_has_vanished
                && let Some(vanished_collection) = sync_collection.vanished_collection()
            {
                vanished = server
                    .store()
                    .vanished(account_id, vanished_collection.into(), Query::Since(id))
                    .await
                    .caused_by(trc::location!())?;
                total_changes += vanished.len();
            }

            // Truncate changes
            if total_changes > limit {
                let mut offset = limit * seq as usize;
                let mut total_changes = 0;

                // Add vanished items to response
                for item in vanished {
                    if offset > 0 {
                        offset -= 1;
                    } else if total_changes < limit {
                        response.add_response(Response::new_status([item], StatusCode::NOT_FOUND));
                        total_changes += 1;
                    } else {
                        *is_sync_limited = true;
                    }
                }

                // Add items to document set
                for document_ids in [&mut display_containers, &mut display_children]
                    .into_iter()
                    .flatten()
                {
                    let mut new_document_ids = RoaringBitmap::new();
                    for id in document_ids.iter() {
                        if offset > 0 {
                            offset -= 1;
                        } else if total_changes < limit {
                            new_document_ids.insert(id);
                            total_changes += 1;
                        } else {
                            *is_sync_limited = true;
                        }
                    }
                    *document_ids = new_document_ids;
                }

                if *is_sync_limited {
                    response.set_sync_token(Urn::Sync { id, seq: seq + 1 }.to_string());
                }
            } else {
                // Add vanished items to response
                for item in vanished {
                    response.add_response(Response::new_status([item], StatusCode::NOT_FOUND));
                }
            }

            if !*is_sync_limited {
                response.set_sync_token(resources.sync_token());
            }

            true
        }
        SyncType::Initial => {
            response.set_sync_token(resources.sync_token());
            false
        }
        SyncType::None => false,
    };

    let mut results = Vec::new();
    if let Some(resource) = resource.resource {
        results = resources
            .subtree_with_depth(resource, query.depth)
            .filter(|item| {
                display_containers.as_ref().is_none_or(|containers| {
                    if container_has_children {
                        if item.is_container() {
                            containers.contains(item.document_id())
                        } else {
                            display_children
                                .as_ref()
                                .is_some_and(|children| children.contains(item.document_id()))
                        }
                    } else {
                        containers.contains(item.document_id())
                    }
                }) && (!query.depth_no_root || item.path() != resource)
            })
            .map(|item| PropFindItem::new(resources.format_resource(item), account_id, item))
            .collect::<Vec<_>>();
    } else {
        if !query.depth_no_root && query.sync_type.is_none_or_initial() {
            server
                .prepare_principal_propfind_response(
                    access_token,
                    collection_container,
                    [account_id].into_iter(),
                    &query.propfind,
                    response,
                )
                .await?;
        }

        if query.depth != 0 {
            results = resources
                .tree_with_depth(query.depth - 1)
                .filter(|item| {
                    display_containers.as_ref().is_none_or(|containers| {
                        if container_has_children {
                            if item.is_container() {
                                containers.contains(item.document_id())
                            } else {
                                display_children
                                    .as_ref()
                                    .is_some_and(|children| children.contains(item.document_id()))
                            }
                        } else {
                            containers.contains(item.document_id())
                        }
                    })
                })
                .map(|item| PropFindItem::new(resources.format_resource(item), account_id, item))
                .collect::<Vec<_>>();

            // Assisted discovery:
            // If 'bob' has access to 'jane' and `bill` calendars, a query to '/dav/cal/bob' will return:
            //    - /dav/cal/bob/default
            //    - /dav/cal/jane/default
            //    - /dav/cal/bill/default
            // This is invalid but it's the only workaround for clients which do not support multiple home-sets
            if server.core.groupware.assisted_discovery
                && !is_sync
                && account_id == access_token.account_id()
                && matches!(
                    sync_collection,
                    SyncCollection::Calendar | SyncCollection::AddressBook
                )
            {
                for shared_account_id in access_token.all_ids_by_collection(collection_container) {
                    if shared_account_id == access_token.account_id() {
                        continue;
                    }
                    let shared_resources = data
                        .resources(server, access_token, shared_account_id, sync_collection)
                        .await
                        .caused_by(trc::location!())?;
                    let shared_containers =
                        (!access_token.is_member(shared_account_id)).then(|| {
                            shared_resources.shared_containers(
                                access_token,
                                [if container_has_children {
                                    Acl::ReadItems
                                } else {
                                    Acl::Read
                                }],
                                true,
                            )
                        });
                    if shared_containers
                        .as_ref()
                        .is_none_or(|containers| !containers.is_empty())
                    {
                        results.extend(
                            shared_resources
                                .tree_with_depth(query.depth - 1)
                                .filter(|item| {
                                    item.is_container()
                                        && shared_containers.as_ref().is_none_or(|containers| {
                                            containers.contains(item.document_id())
                                        })
                                })
                                .map(|item| {
                                    PropFindItem::new(
                                        shared_resources.format_resource(item),
                                        shared_account_id,
                                        item,
                                    )
                                }),
                        );
                    }
                }
            }
        }
    }

    Ok(results)
}

#[allow(clippy::too_many_arguments)]
async fn multiget(
    server: &Server,
    access_token: &AccessToken,
    collection_container: Collection,
    collection_children: Collection,
    sync_collection: SyncCollection,
    data: &mut PropFindData,
    response: &mut MultiStatus,
    hrefs: Vec<String>,
) -> crate::Result<Vec<PropFindItem>> {
    let mut paths = Vec::with_capacity(hrefs.len() * 2);
    let mut shared_folders_by_account: AHashMap<u32, Arc<RoaringBitmap>> =
        AHashMap::with_capacity(3);

    for item in hrefs {
        let resource = match server
            .validate_uri(access_token, &item)
            .await
            .and_then(|r| r.into_owned_uri())
        {
            Ok(resource) => resource,
            Err(DavError::Code(code)) => {
                response.add_response(Response::new_status([item], code));
                continue;
            }
            Err(err) => {
                return Err(err);
            }
        };

        let account_id = resource.account_id;
        let resources = data
            .resources(server, access_token, account_id, sync_collection)
            .await
            .caused_by(trc::location!())?;

        let document_ids = if !access_token.is_member(account_id) {
            if let Some(document_ids) = shared_folders_by_account.get(&account_id) {
                document_ids.clone().into()
            } else {
                let document_ids = Arc::new(resources.shared_containers(
                    access_token,
                    [if collection_children == collection_container {
                        Acl::ReadItems
                    } else {
                        Acl::Read
                    }],
                    true,
                ));
                shared_folders_by_account.insert(account_id, document_ids.clone());
                document_ids.into()
            }
        } else {
            None
        };

        if let Some(resource) = resource.resource.and_then(|name| resources.by_path(name)) {
            if !resource.is_container() {
                if document_ids
                    .as_ref()
                    .is_none_or(|docs| docs.contains(resource.parent_id().unwrap()))
                {
                    paths.push(PropFindItem::new(
                        resources.format_resource(resource),
                        account_id,
                        resource,
                    ));
                } else {
                    response.add_response(
                        Response::new_status([item], StatusCode::FORBIDDEN)
                            .with_response_description(
                                "Not enough permissions to access this shared resource",
                            ),
                    );
                }
            } else {
                response.add_response(
                    Response::new_status([item], StatusCode::FORBIDDEN)
                        .with_response_description("Multiget not allowed for collections"),
                );
            }
        } else {
            response.add_response(Response::new_status([item], StatusCode::NOT_FOUND));
        }
    }

    Ok(paths)
}

impl PropFindItem {
    pub fn new(name: String, account_id: u32, resource: DavResourcePath<'_>) -> Self {
        Self {
            name,
            account_id,
            document_id: resource.document_id(),
            parent_id: resource.parent_id(),
            is_container: resource.is_container(),
        }
    }
}

impl PropFindData {
    pub fn new() -> Self {
        Self {
            accounts: AHashMap::with_capacity(2),
        }
    }

    pub async fn quota(
        &mut self,
        server: &Server,
        account_id: u32,
    ) -> trc::Result<PropFindAccountQuota> {
        let data = self.accounts.entry(account_id).or_default();

        if data.quota.is_none() {
            data.quota = server.dav_quota(account_id).await?.into();
        }

        Ok(data.quota.clone().unwrap())
    }

    pub async fn owner(
        &mut self,
        server: &Server,
        account_info: &AccountCache,
        account_id: u32,
    ) -> trc::Result<Href> {
        let data = self.accounts.entry(account_id).or_default();

        if data.owner.is_none() {
            data.owner = server
                .owner_href(account_info, account_id)
                .await
                .caused_by(trc::location!())?
                .into();
        }

        Ok(data.owner.clone().unwrap())
    }

    pub async fn resources(
        &mut self,
        server: &Server,
        access_token: &AccessToken,
        account_id: u32,
        sync_collection: SyncCollection,
    ) -> trc::Result<Arc<DavResources>> {
        let data = self.accounts.entry(account_id).or_default();

        if data.resources.is_none() {
            let resources = server
                .fetch_dav_resources(access_token.account_id(), account_id, sync_collection)
                .await
                .caused_by(trc::location!())?;
            data.resources = resources.into();
        }

        Ok(data.resources.clone().unwrap())
    }

    pub async fn locks(
        &mut self,
        server: &Server,
        account_id: u32,
        collection_container: Collection,
        item: &PropFindItem,
    ) -> trc::Result<Option<Vec<ActiveLock>>> {
        let data = self.accounts.entry(account_id).or_default();

        if data.locks.is_none() && !data.locks_not_found {
            data.locks = server
                .in_memory_store()
                .key_get::<Archive<AlignedBytes>>(
                    build_lock_key(account_id, collection_container).as_slice(),
                )
                .await
                .caused_by(trc::location!())?;
            if data.locks.is_none() {
                data.locks_not_found = true;
            }
        }

        if let Some(lock_data) = &data.locks {
            let base_uri = dav_base_uri(&item.name).unwrap_or_default();
            lock_data.unarchive::<LockData>().map(|locks| {
                locks
                    .find_locks(&item.name.strip_prefix(base_uri).unwrap()[1..], false)
                    .iter()
                    .map(|(path, lock)| lock.to_active_lock(format!("{base_uri}/{path}")))
                    .collect::<Vec<_>>()
                    .into()
            })
        } else {
            Ok(None)
        }
    }
}

pub(crate) trait SyncTokenUrn {
    fn sync_token(&self) -> String;
}

impl SyncTokenUrn for DavResources {
    fn sync_token(&self) -> String {
        Urn::Sync {
            id: self.highest_change_id,
            seq: 0,
        }
        .to_string()
    }
}

async fn add_base_collection_response(
    server: &Server,
    request: &PropFind,
    collection: Collection,
    access_token: &AccessToken,
    response: &mut MultiStatus,
) -> trc::Result<()> {
    let properties = match request {
        PropFind::PropName => {
            response.add_response(Response::new_propstat(
                DavResourceName::from(collection).collection_path(),
                vec![PropStat::new_list(vec![
                    DavPropertyValue::empty(DavProperty::WebDav(WebDavProperty::ResourceType)),
                    DavPropertyValue::empty(DavProperty::WebDav(
                        WebDavProperty::CurrentUserPrincipal,
                    )),
                    DavPropertyValue::empty(DavProperty::WebDav(
                        WebDavProperty::SupportedReportSet,
                    )),
                ])],
            ));
            return Ok(());
        }
        PropFind::AllProp(_) => [
            DavProperty::WebDav(WebDavProperty::ResourceType),
            DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal),
            DavProperty::WebDav(WebDavProperty::SupportedReportSet),
        ]
        .as_slice(),
        PropFind::Prop(items) => items,
    };

    let mut fields = Vec::with_capacity(properties.len());
    let mut fields_not_found = Vec::new();
    let account_info = server
        .account(access_token.account_id())
        .await
        .caused_by(trc::location!())?;

    for prop in properties {
        match &prop {
            DavProperty::WebDav(WebDavProperty::ResourceType) => {
                fields.push(DavPropertyValue::new(
                    prop.clone(),
                    vec![ResourceType::Collection],
                ));
            }
            DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal) => {
                fields.push(DavPropertyValue::new(
                    prop.clone(),
                    vec![account_info.current_user_principal()],
                ));
            }
            DavProperty::Principal(PrincipalProperty::CalendarHomeSet) => {
                let hrefs = build_home_set(
                    server,
                    access_token,
                    account_info.name(),
                    access_token.account_id(),
                    true,
                )
                .await
                .caused_by(trc::location!())?;

                fields.push(DavPropertyValue::new(prop.clone(), hrefs));
                response.set_namespace(Namespace::CalDav);
            }
            DavProperty::Principal(PrincipalProperty::AddressbookHomeSet) => {
                let hrefs = build_home_set(
                    server,
                    access_token,
                    account_info.name(),
                    access_token.account_id(),
                    false,
                )
                .await
                .caused_by(trc::location!())?;

                fields.push(DavPropertyValue::new(prop.clone(), hrefs));
                response.set_namespace(Namespace::CardDav);
            }
            DavProperty::WebDav(WebDavProperty::SupportedReportSet) => {
                let reports = match collection {
                    Collection::Principal => ReportSet::principal(),
                    Collection::Calendar | Collection::CalendarEvent => ReportSet::calendar(),
                    Collection::AddressBook | Collection::ContactCard => ReportSet::addressbook(),
                    _ => ReportSet::file(),
                };

                fields.push(DavPropertyValue::new(prop.clone(), reports));
            }
            _ => {
                response.set_namespace(prop.namespace());
                fields_not_found.push(DavPropertyValue::empty(prop.clone()));
            }
        }
    }

    let mut prop_stat = Vec::with_capacity(2);

    if !fields.is_empty() {
        prop_stat.push(PropStat::new_list(fields));
    }

    if !fields_not_found.is_empty() {
        prop_stat.push(PropStat::new_list(fields_not_found).with_status(StatusCode::NOT_FOUND));
    }

    response.add_response(Response::new_propstat(
        DavResourceName::from(collection).collection_path(),
        prop_stat,
    ));

    Ok(())
}
