// SPDX-License-Identifier: GPL-3.0-or-later
// File: ./src/client/core.rs
/*
File: ./src/client/core.rs

Core networking and high-level CalDAV client APIs.

This file focuses on:
- Constructing the HTTP/CalDAV client
- High-level operations (connect_with_fallback, discover, fetch tasks,
  create/update/delete wrappers that journal + trigger sync)
- Utilities used across the client

Journal processing and low-level sync step implementations (handle_create,
handle_update, handle_delete, handle_move, sync_journal, conflict resolution,
etc.) have been moved into `src/client/sync.rs` to reduce duplication and keep
responsibilities clear.
*/

use crate::cache::Cache;

use crate::client::auth::DynamicAuthLayer;
use crate::client::cert::NoVerifier;
use crate::client::middleware::{UserAgentLayer, UserAgentService};
use crate::config::Config;
use crate::context::AppContext;
use crate::journal::{Action, Journal};
use crate::model::{CalendarListEntry, IcsAdapter, Task};
use crate::storage::{LocalCalendarRegistry, LocalStorage};

use http::{Request, StatusCode};
use libdav::caldav::{FindCalendarHomeSet, FindCalendars, GetCalendarResources};
use libdav::dav::{Delete, GetProperty, ListResources, Propfind, PutResource};
use libdav::dav::{WebDavClient, WebDavError};
use libdav::{CalDavClient, PropertyName, names};
use roxmltree::Document;

use anyhow;

fn xml_escape(s: &str) -> String {
    s.replace('&', "&amp;")
        .replace('<', "&lt;")
        .replace('>', "&gt;")
        .replace('"', "&quot;")
        .replace('\'', "&apos;")
}
use futures::stream::{self, StreamExt};
use http::Uri;
use hyper_rustls::HttpsConnectorBuilder;
use hyper_util::client::legacy::Client;
use hyper_util::rt::TokioExecutor;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;

#[cfg(not(target_os = "android"))]
use rustls_native_certs;

use tower_layer::Layer;

// Re-exports used elsewhere in the crate
pub const GET_CTAG: PropertyName = PropertyName::new("http://calendarserver.org/ns/", "getctag");
pub const APPLE_COLOR: PropertyName =
    PropertyName::new("http://apple.com/ns/ical/", "calendar-color");

use crate::client::FollowRedirectLayer;
use crate::client::FollowRedirectService;
use crate::client::auth::DynamicAuthService;

// Concrete HttpsClient type used throughout the crate. This is a FollowRedirect
// wrapper around the DynamicAuthService -> UserAgentService -> hyper Client.
pub(crate) type HttpsClient = FollowRedirectService<
    DynamicAuthService<
        UserAgentService<
            Client<
                hyper_rustls::HttpsConnector<hyper_util::client::legacy::connect::HttpConnector>,
                String,
            >,
        >,
    >,
>;

// -----------------------------
// Test hooks (test-only)
// These are provided so unit/integration tests can inject deterministic
// behavior into network/fetch paths. Kept in this module so tests that refer
// to `cfait::client::core::test_hooks::...` keep working.
#[cfg(any(test, feature = "test_hooks"))]
pub mod test_hooks {
    use super::Task;
    use crate::journal::Action;
    use std::sync::{Mutex, OnceLock};

    pub type FetchRemoteHook = Box<dyn Fn(&str) -> Option<Task> + Send + Sync + 'static>;
    pub type ForceSyncErrorHook =
        Box<dyn Fn(&Action) -> Option<anyhow::Error> + Send + Sync + 'static>;

    /// Test hook to simulate fetch_remote_task responses in unit tests.
    pub static TEST_FETCH_REMOTE_HOOK: OnceLock<Mutex<Option<FetchRemoteHook>>> = OnceLock::new();

    /// Test hook to force a synthetic sync error for a given Action during tests.
    pub static TEST_FORCE_SYNC_ERROR: OnceLock<Mutex<Option<ForceSyncErrorHook>>> = OnceLock::new();
}

#[cfg(any(test, feature = "test_hooks"))]
pub use test_hooks::{
    FetchRemoteHook, ForceSyncErrorHook, TEST_FETCH_REMOTE_HOOK, TEST_FORCE_SYNC_ERROR,
};

// -----------------------------

pub(crate) fn strip_host(href: &str) -> String {
    if href.starts_with("local://") {
        return href.to_string();
    }
    if let Ok(uri) = href.parse::<Uri>()
        && (uri.scheme().is_some() || uri.authority().is_some())
    {
        let path = uri.path();
        if path.is_empty() {
            return href.to_string();
        }
        return uri
            .path_and_query()
            .map(|pq| pq.as_str().to_string())
            .unwrap_or_else(|| path.to_string());
    }
    href.to_string()
}

// -----------------------------
// High-level RustyClient - network construction and high-level APIs.
// Lower-level sync steps are implemented in src/client/sync.rs (impl RustyClient there).
#[derive(Clone, Debug)]
pub struct RustyClient {
    pub client: Option<CalDavClient<HttpsClient>>,
    pub ctx: Arc<dyn AppContext>,
}

impl RustyClient {
    /// Construct a new client. If `url` is empty, this returns an "offline" client
    /// (client == None) which is used for local-only operations.
    pub fn new(
        ctx: Arc<dyn AppContext>,
        url: &str,
        user: &str,
        pass: &str,
        insecure: bool,
        client_type: Option<&str>,
    ) -> anyhow::Result<Self> {
        if url.is_empty() {
            return Ok(Self {
                client: None,
                ctx: ctx.clone(),
            });
        }

        let uri: Uri = url
            .parse()
            .map_err(|e: http::uri::InvalidUri| anyhow::anyhow!("Invalid URI: {}", e))?;

        let tls_config_builder = rustls::ClientConfig::builder();
        let tls_config = if insecure {
            tls_config_builder
                .dangerous()
                .with_custom_certificate_verifier(Arc::new(NoVerifier))
                .with_no_client_auth()
        } else {
            #[cfg(not(target_os = "android"))]
            {
                let mut root_store = rustls::RootCertStore::empty();
                let result = rustls_native_certs::load_native_certs();
                root_store.add_parsable_certificates(result.certs);
                if root_store.is_empty() {
                    return Err(anyhow::anyhow!(rust_i18n::t!("error_no_certs").to_string()));
                }
                tls_config_builder
                    .with_root_certificates(root_store)
                    .with_no_client_auth()
            }

            #[cfg(target_os = "android")]
            {
                let mut root_store = rustls::RootCertStore::empty();
                root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
                tls_config_builder
                    .with_root_certificates(root_store)
                    .with_no_client_auth()
            }
        };

        let https_connector = HttpsConnectorBuilder::new()
            .with_tls_config(tls_config)
            .https_or_http()
            .enable_http1()
            .build();

        let http_client = Client::builder(TokioExecutor::new()).build(https_connector);

        // Build a deterministic User-Agent string
        let version = env!("CARGO_PKG_VERSION");
        let ua_string = if let Some(ctype) = client_type {
            format!("Cfait/{} ({})", version, ctype)
        } else {
            format!("Cfait/{}", version)
        };

        let ua_client = UserAgentLayer::new(ua_string).layer(http_client);
        let auth_client =
            DynamicAuthLayer::new(user.to_string(), pass.to_string()).layer(ua_client);
        let redirect_client = FollowRedirectLayer::new(10).layer(auth_client);

        let webdav = WebDavClient::new(uri, redirect_client.clone());
        let caldav = CalDavClient::new(webdav);

        Ok(Self {
            client: Some(caldav),
            ctx,
        })
    }

    /// Attempts to automatically discover the primary calendar for the user.
    /// Returns a path string on success.
    pub async fn discover_calendar(&self) -> anyhow::Result<String> {
        if let Some(client) = &self.client {
            let base_path = client.base_url().path().to_string();
            // Fast heuristic: if any resource in base path ends with .ics treat it as calendar root
            if let Ok(response) = client.request(ListResources::new(&base_path)).await
                && response.resources.iter().any(|r| r.href.ends_with(".ics"))
            {
                return Ok(base_path);
            }
            // Fallback to principal/home-set discovery
            if let Ok(Some(principal)) = client.find_current_user_principal().await
                && let Ok(response) = client
                    .request(FindCalendarHomeSet::new(principal.path()))
                    .await
                && let Some(home_url) = response.home_sets.first()
                && let Ok(cals_resp) = client.request(FindCalendars::new(home_url.path())).await
                && let Some(first) = cals_resp.calendars.first()
            {
                return Ok(first.href.clone());
            }
            Ok(base_path)
        } else {
            Err(anyhow::anyhow!("Offline"))
        }
    }

    /// The primary entry point for UIs to connect.
    /// This function handles connection, discovery, fallback to cache on error,
    /// and initial data loading.
    pub async fn connect_with_fallback(
        ctx: Arc<dyn AppContext>,
        config: Config,
        client_type: Option<&str>,
    ) -> anyhow::Result<(
        Self,
        Vec<CalendarListEntry>,
        Vec<Task>,
        Option<String>,
        Option<String>,
    )> {
        // Clone config so we can update/save if we detect an auto-corrected root.
        let mut config_for_saving = config.clone();

        let client = Self::new(
            ctx.clone(),
            &config.url,
            &config.username,
            &config.password,
            config.allow_insecure_certs,
            client_type,
        )?;

        // Limit timeout to ensure a clean fallback in problematic scenarios.
        // If the queue is massive, we don't want to block the initial load forever.
        let _ =
            tokio::time::timeout(std::time::Duration::from_secs(10), client.sync_journal()).await;

        // Attempt to fetch calendars and optionally auto-correct URL/prefixes
        let ((calendars, corrected_url_opt), warning) = match client.get_calendars().await {
            Ok((c, corrected_url)) => {
                if c.is_empty() {
                    let helpful_msg = rust_i18n::t!("error_no_calendars_found").to_string();
                    ((c, corrected_url), Some(helpful_msg))
                } else {
                    let _ = Cache::save_calendars(client.ctx.as_ref(), &c);
                    ((c, corrected_url), None)
                }
            }
            Err(e) => {
                let error_msg = e.to_string();
                let mut specific_warning = None;
                if error_msg.contains("InvalidCertificate") {
                    return Err(anyhow::anyhow!(
                        "{}",
                        rust_i18n::t!("error_invalid_tls", error = error_msg)
                    ));
                }
                if error_msg.contains("Unauthorized")
                    || error_msg.contains("Forbidden")
                    || error_msg.contains("401")
                    || error_msg.contains("403")
                {
                    specific_warning = Some(rust_i18n::t!("error_auth_failed").to_string());
                } else if error_msg.contains("NotFound") || error_msg.contains("404") {
                    specific_warning = Some(rust_i18n::t!("error_404_not_found").to_string());
                } else if error_msg.contains("Timeout") {
                    specific_warning = Some(rust_i18n::t!("error_timeout").to_string());
                }

                let cals = Cache::load_calendars(client.ctx.as_ref()).unwrap_or_default();

                let final_warning = specific_warning.unwrap_or_else(|| {
                    rust_i18n::t!("error_offline_fallback", error = error_msg.clone()).to_string()
                });

                ((cals, None), Some(final_warning))
            }
        };

        // Determine active/default calendar href (if configured)
        let mut active_href: Option<String> = None;
        if let Some(def_cal) = &config.default_calendar
            && let Some(found) = calendars
                .iter()
                .find(|c| c.name == *def_cal || c.href == *def_cal)
        {
            active_href = Some(found.href.clone());
        }

        let mut needs_config_save = false;

        if active_href.is_none()
            && warning.is_none()
            && let Ok(href) = client.discover_calendar().await
        {
            active_href = Some(href.clone());
            config_for_saving.default_calendar = Some(href);
            needs_config_save = true;
        }

        // If discovery produced a corrected root URL, persist it asynchronously.
        if let Some(corrected_url) = corrected_url_opt {
            config_for_saving.url = corrected_url;
            needs_config_save = true;
        }

        if needs_config_save {
            let ctx_clone = client.ctx.clone();
            tokio::spawn(async move {
                if let Err(e) = config_for_saving.save(ctx_clone.as_ref()) {
                    #[cfg(not(target_os = "android"))]
                    eprintln!("[Warning] Failed to auto-save config corrections: {}", e);
                    #[cfg(target_os = "android")]
                    log::warn!("Failed to auto-save config corrections: {}", e);
                }
            });
        }

        // If no warning, fetch tasks for the active calendar (best-effort)
        let tasks = if warning.is_none() {
            if let Some(ref h) = active_href {
                client.get_tasks(h).await.unwrap_or_default()
            } else {
                vec![]
            }
        } else if let Some(ref h) = active_href {
            // Fallback: load tasks from cache + apply journal if present
            let (mut t, _) = Cache::load(client.ctx.as_ref(), h).unwrap_or((vec![], None));
            Journal::apply_to_tasks(client.ctx.as_ref(), &mut t, h);
            t
        } else {
            vec![]
        };

        Ok((client, calendars, tasks, active_href, warning))
    }

    // Helper to encapsulate the core discovery logic (used by get_calendars)
    async fn perform_calendar_discovery(
        &self,
        _discovery_path: &str,
    ) -> anyhow::Result<Vec<CalendarListEntry>> {
        let client = self
            .client
            .as_ref()
            .ok_or_else(|| anyhow::anyhow!("Offline"))?;

        let principal_res = client.find_current_user_principal().await?;
        let Some(principal) = principal_res else {
            return Err(anyhow::anyhow!(
                rust_i18n::t!("error_no_principal").to_string()
            ));
        };

        let home_set_resp = client
            .request(FindCalendarHomeSet::new(principal.path()))
            .await?;

        let home_url = home_set_resp
            .home_sets
            .first()
            .ok_or_else(|| anyhow::anyhow!(rust_i18n::t!("error_no_home_set").to_string()))?;

        let cals_resp = client.request(FindCalendars::new(home_url.path())).await?;

        let mut calendars = Vec::new();
        for col in cals_resp.calendars {
            let name = client
                .request(GetProperty::new(&col.href, &names::DISPLAY_NAME))
                .await
                .ok()
                .and_then(|r| r.value)
                .unwrap_or_else(|| col.href.clone());

            let color = client
                .request(GetProperty::new(&col.href, &APPLE_COLOR))
                .await
                .ok()
                .and_then(|r| r.value);

            let (comps, can_write) = self
                .get_supported_components(&col.href)
                .await
                .unwrap_or_else(|_| (Vec::new(), true));

            if can_write && comps.iter().any(|c| c.eq_ignore_ascii_case("VTODO")) {
                calendars.push(CalendarListEntry {
                    name,
                    href: col.href,
                    color,
                });
            }
        }
        Ok(calendars)
    }

    /// Get calendars (remote + local), with optional auto-corrected URL returned.
    pub async fn get_calendars(&self) -> anyhow::Result<(Vec<CalendarListEntry>, Option<String>)> {
        if let Some(_client) = &self.client {
            // attempt discovery at configured path
            let user_configured_path = self.client.as_ref().unwrap().base_url().path();
            let mut corrected_url = None;

            let mut calendars = self
                .perform_calendar_discovery(user_configured_path)
                .await?;

            // Fallback: if nothing found, try server root and offer corrected root URL
            if calendars.is_empty()
                && user_configured_path != "/"
                && let Ok(fallback) = self.perform_calendar_discovery("/").await
                && !fallback.is_empty()
            {
                calendars = fallback;
                let base_uri = self.client.as_ref().unwrap().base_url();
                if let (Some(scheme), Some(authority)) = (base_uri.scheme(), base_uri.authority()) {
                    corrected_url = Some(format!("{}://{}", scheme, authority));
                }
            }

            // Include local calendars; but only show recovery/trash if they contain tasks
            if let Ok(local_cals) = LocalCalendarRegistry::load(self.ctx.as_ref()) {
                for local_cal in local_cals {
                    if local_cal.href == "local://recovery"
                        || local_cal.href == crate::storage::LOCAL_TRASH_HREF
                    {
                        if let Ok(tasks) =
                            LocalStorage::load_for_href(self.ctx.as_ref(), &local_cal.href)
                            && !tasks.is_empty()
                        {
                            calendars.push(local_cal);
                        }
                    } else {
                        calendars.push(local_cal);
                    }
                }
            }

            Ok((calendars, corrected_url))
        } else {
            // Offline mode: return cached + local calendars
            let mut calendars = Cache::load_calendars(self.ctx.as_ref()).unwrap_or_default();
            if let Ok(local_cals) = LocalCalendarRegistry::load(self.ctx.as_ref()) {
                for local_cal in local_cals {
                    if calendars.iter().any(|c| c.href == local_cal.href) {
                        continue;
                    }
                    if local_cal.href == "local://recovery"
                        || local_cal.href == crate::storage::LOCAL_TRASH_HREF
                    {
                        if let Ok(tasks) =
                            LocalStorage::load_for_href(self.ctx.as_ref(), &local_cal.href)
                            && !tasks.is_empty()
                        {
                            calendars.push(local_cal);
                        }
                    } else {
                        calendars.push(local_cal);
                    }
                }
            }
            Ok((calendars, None))
        }
    }

    pub async fn get_supported_components(
        &self,
        calendar_href: &str,
    ) -> anyhow::Result<(Vec<String>, bool)> {
        if let Some(_client) = &self.client {
            let privilege_set_prop = PropertyName::new("DAV:", "current-user-privilege-set");
            let req = Propfind::new(calendar_href)
                .with_properties(&[
                    &names::SUPPORTED_CALENDAR_COMPONENT_SET,
                    &privilege_set_prop,
                ])
                .with_depth(libdav::Depth::Zero);
            let response = self.client.as_ref().unwrap().request(req).await?;
            let xml_str = std::str::from_utf8(&response.body)?;
            let doc = Document::parse(xml_str)?;
            let mut components = Vec::new();
            let mut can_write = false;
            let mut has_privilege_set = false;

            for node in doc.descendants() {
                if node.tag_name().name().eq_ignore_ascii_case("comp")
                    && let Some(name) = node.attribute("name")
                {
                    components.push(name.to_uppercase());
                }
                if node
                    .tag_name()
                    .name()
                    .eq_ignore_ascii_case("current-user-privilege-set")
                {
                    has_privilege_set = true;
                }
                if node.tag_name().name().eq_ignore_ascii_case("write")
                    || node.tag_name().name().eq_ignore_ascii_case("write-content")
                    || node.tag_name().name().eq_ignore_ascii_case("bind")
                    || node.tag_name().name().eq_ignore_ascii_case("all")
                {
                    // DAV:all (RFC 3744 section 3.13) is the aggregate of all
                    // privileges and therefore implies write access.
                    can_write = true;
                }
            }
            if !has_privilege_set {
                can_write = true;
            }
            Ok((components, can_write))
        } else {
            Err(anyhow::anyhow!("Offline"))
        }
    }

    pub async fn get_companion_events(
        &self,
        calendar_href: &str,
        task_uid: Option<&str>,
    ) -> anyhow::Result<Vec<String>> {
        let client = self
            .client
            .as_ref()
            .ok_or_else(|| anyhow::anyhow!("Offline"))?;
        let path = strip_host(calendar_href);

        let body = if let Some(uid) = task_uid {
            format!(
                r#"<?xml version="1.0" encoding="utf-8" ?>
<C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
  <D:prop>
    <D:getetag/>
  </D:prop>
  <C:filter>
    <C:comp-filter name="VCALENDAR">
      <C:comp-filter name="VEVENT">
        <C:prop-filter name="X-CFAIT-TASK-UID">
          <C:text-match collation="i;ascii-casemap">{}</C:text-match>
        </C:prop-filter>
      </C:comp-filter>
    </C:comp-filter>
  </C:filter>
</C:calendar-query>"#,
                xml_escape(uid)
            )
        } else {
            r#"<?xml version="1.0" encoding="utf-8" ?>
<C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
  <D:prop>
    <D:getetag/>
  </D:prop>
  <C:filter>
    <C:comp-filter name="VCALENDAR">
      <C:comp-filter name="VEVENT">
        <C:prop-filter name="X-CFAIT-TASK-UID">
          <C:is-defined/>
        </C:prop-filter>
      </C:comp-filter>
    </C:comp-filter>
  </C:filter>
</C:calendar-query>"#
                .to_string()
        };

        let base = client.base_url();
        let scheme = base.scheme_str().unwrap_or("https");
        let authority = base.authority().map(|a| a.as_str()).unwrap_or("");

        let clean_path = if path.starts_with('/') {
            path.clone()
        } else {
            format!("/{}", path)
        };
        let absolute_destination = format!("{}://{}{}", scheme, authority, clean_path);

        let req = Request::builder()
            .method("REPORT")
            .uri(absolute_destination)
            .header("Content-Type", "application/xml; charset=utf-8")
            .header("Depth", "1")
            .body(body)
            .map_err(|e| anyhow::anyhow!("Request build failed: {}", e))?;

        let (parts, body_bytes) = client
            .webdav_client
            .request_raw(req)
            .await
            .map_err(|e| anyhow::anyhow!("REPORT failed: {:?}", e))?;

        // Fallback: Strict CalDAV servers might reject REPORT with custom X-properties with 403 or 400.
        if !parts.status.is_success() && parts.status != StatusCode::MULTI_STATUS {
            let list_resp = client.request(ListResources::new(&path)).await?;
            let mut hrefs = Vec::new();
            for res in list_resp.resources {
                let filename = res.href.split('/').next_back().unwrap_or("");
                if filename.starts_with("evt-") && filename.ends_with(".ics") {
                    if let Some(uid) = task_uid {
                        if filename.starts_with(&format!("evt-{}", uid)) {
                            hrefs.push(res.href);
                        }
                    } else {
                        hrefs.push(res.href);
                    }
                }
            }
            return Ok(hrefs);
        }

        let xml_str = std::str::from_utf8(&body_bytes).unwrap_or("");
        let mut hrefs = Vec::new();

        if let Ok(doc) = roxmltree::Document::parse(xml_str) {
            for node in doc.descendants() {
                if node.tag_name().name().eq_ignore_ascii_case("href")
                    && let Some(text) = node.text()
                {
                    hrefs.push(text.to_string());
                }
            }
        }

        Ok(hrefs)
    }

    pub(crate) async fn sync_companion_event(
        &self,
        task: &Task,
        config_enabled: bool,
        delete_on_completion: bool,
        is_delete_intent: bool,
    ) -> bool {
        // Local calendars don't have server-side events
        if task.calendar_href.starts_with("local://") {
            return false;
        }

        let should_create_events = task.create_event.unwrap_or(config_enabled);
        let base_uid = format!("evt-{}", task.uid);

        let cal_path = if task.calendar_href.ends_with('/') {
            task.calendar_href.clone()
        } else {
            let p = strip_host(&task.href);
            if let Some(idx) = p.rfind('/') {
                p[..=idx].to_string()
            } else {
                task.calendar_href.clone()
            }
        };

        let client = match &self.client {
            Some(c) => c,
            None => return false,
        };

        let has_calendar_data =
            task.due.is_some() || task.dtstart.is_some() || !task.sessions.is_empty();
        let keep_completed = !delete_on_completion && task.status.is_done();

        let should_delete = is_delete_intent
            || (delete_on_completion && task.status.is_done())
            || (!has_calendar_data && !keep_completed)
            || !should_create_events;

        if !should_create_events
            && !is_delete_intent
            && !delete_on_completion
            && task.create_event.is_none()
        {
            return true;
        }

        // Use REPORT to find ground truth of companion events
        let existing_hrefs = self
            .get_companion_events(&cal_path, Some(&task.uid))
            .await
            .unwrap_or_default();
        let mut existing_filenames: std::collections::HashSet<String> = existing_hrefs
            .iter()
            .map(|h| h.split('/').next_back().unwrap_or("").to_string())
            .collect();

        let mut futures: Vec<futures::future::BoxFuture<'_, Result<(), ()>>> = Vec::new();

        let generated_events = if should_delete {
            vec![]
        } else {
            IcsAdapter::to_event_ics(task)
        };

        // 1. PUT all generated events
        for (suffix, ics_body) in generated_events.iter() {
            let event_filename = format!("{}{}.ics", base_uid, suffix);

            // Remove from existing so we know what's left over to delete
            existing_filenames.remove(&event_filename);

            let event_path = format!("{}{}", strip_host(&cal_path), event_filename);
            let c = client.clone();
            let body_clone = ics_body.clone();

            futures.push(Box::pin(async move {
                let create_req = PutResource::new(&event_path)
                    .create(body_clone.clone(), "text/calendar; charset=utf-8");
                match c.request(create_req).await {
                    Ok(_) => Ok(()),
                    Err(WebDavError::BadStatusCode(http::StatusCode::PRECONDITION_FAILED))
                    | Err(WebDavError::PreconditionFailed(_)) => {
                        let update_req = PutResource::new(&event_path).update(
                            body_clone,
                            "text/calendar; charset=utf-8",
                            "",
                        );
                        if c.request(update_req).await.is_err() {
                            Err(())
                        } else {
                            Ok(())
                        }
                    }
                    Err(_) => Err(()),
                }
            }));
        }

        // 2. DELETE obsolete events that we actually know exist
        for obsolete_filename in existing_filenames {
            let event_path = format!("{}{}", strip_host(&cal_path), obsolete_filename);
            let c = client.clone();
            futures.push(Box::pin(async move {
                match c.request(Delete::new(&event_path).force()).await {
                    Ok(_) => Ok(()),
                    Err(WebDavError::BadStatusCode(http::StatusCode::NOT_FOUND)) => Ok(()),
                    Err(_) => Err(()),
                }
            }));
        }

        // 3. FALLBACK FOR LEGACY EVENTS
        // Check old static suffixes just in case they lack the X-CFAIT-TASK-UID property
        let static_suffixes = ["", "-start", "-due"];
        for suffix in static_suffixes {
            let event_filename = format!("{}{}.ics", base_uid, suffix);
            if !generated_events.iter().any(|(s, _)| s == suffix) {
                let event_path = format!("{}{}", strip_host(&cal_path), event_filename);
                let c = client.clone();
                futures.push(Box::pin(async move {
                    match c.request(Delete::new(&event_path).force()).await {
                        Ok(_) => Ok(()),
                        Err(WebDavError::BadStatusCode(http::StatusCode::NOT_FOUND)) => Ok(()),
                        Err(_) => Err(()),
                    }
                }));
            }
        }

        let results = futures::future::join_all(futures).await;
        results.into_iter().all(|r| r.is_ok())
    }

    /// Public wrapper for convenience
    pub async fn sync_task_companion_event(
        &self,
        task: &Task,
        config_enabled: bool,
    ) -> anyhow::Result<bool> {
        let cfg = Config::load(self.ctx.as_ref()).unwrap_or_default();
        let delete_on_completion = cfg.delete_events_on_completion;
        let res = self
            .sync_companion_event(task, config_enabled, delete_on_completion, false)
            .await;
        Ok(res)
    }

    pub async fn sync_multiple_companion_events(
        &self,
        tasks: &[Task],
        config_enabled: bool,
        delete_on_completion: bool,
    ) -> anyhow::Result<usize> {
        let client = self
            .client
            .as_ref()
            .ok_or_else(|| anyhow::anyhow!("Offline"))?;

        let mut by_calendar: HashMap<String, Vec<&Task>> = HashMap::new();
        for task in tasks {
            if !task.calendar_href.starts_with("local://") {
                let cal_path = if task.calendar_href.ends_with('/') {
                    task.calendar_href.clone()
                } else {
                    let p = strip_host(&task.href);
                    if let Some(idx) = p.rfind('/') {
                        p[..=idx].to_string()
                    } else {
                        task.calendar_href.clone()
                    }
                };
                by_calendar.entry(cal_path).or_default().push(task);
            }
        }

        let mut success_count = 0;

        for (cal_path, cal_tasks) in by_calendar {
            let existing_hrefs = self
                .get_companion_events(&cal_path, None)
                .await
                .unwrap_or_default();

            let mut all_existing_filenames: std::collections::HashSet<String> = existing_hrefs
                .into_iter()
                .map(|h| h.split('/').next_back().unwrap_or("").to_string())
                .collect();

            let mut futures: Vec<futures::future::BoxFuture<'_, Result<(), ()>>> = Vec::new();

            for task in cal_tasks {
                let should_create_events = task.create_event.unwrap_or(config_enabled);
                let base_uid = format!("evt-{}", task.uid);

                let has_calendar_data =
                    task.due.is_some() || task.dtstart.is_some() || !task.sessions.is_empty();
                let keep_completed = !delete_on_completion && task.status.is_done();

                let should_delete = (delete_on_completion && task.status.is_done())
                    || (!has_calendar_data && !keep_completed)
                    || !should_create_events;

                if !should_create_events && !delete_on_completion && task.create_event.is_none() {
                    continue;
                }

                // Safely extract just the filenames belonging to THIS task
                let mut task_existing_filenames = std::collections::HashSet::new();
                let mut retain_list = Vec::new();
                for filename in all_existing_filenames.into_iter() {
                    if filename.starts_with(&base_uid) && filename.ends_with(".ics") {
                        task_existing_filenames.insert(filename);
                    } else {
                        retain_list.push(filename);
                    }
                }
                all_existing_filenames = retain_list.into_iter().collect();

                let generated_events = if should_delete {
                    vec![]
                } else {
                    IcsAdapter::to_event_ics(task)
                };

                for (suffix, ics_body) in generated_events.iter() {
                    let event_filename = format!("{}{}.ics", base_uid, suffix);
                    task_existing_filenames.remove(&event_filename);

                    let event_path = format!("{}{}", strip_host(&cal_path), event_filename);
                    let c = client.clone();
                    let body_clone = ics_body.clone();

                    futures.push(Box::pin(async move {
                        let create_req = PutResource::new(&event_path)
                            .create(body_clone.clone(), "text/calendar; charset=utf-8");
                        match c.request(create_req).await {
                            Ok(_) => Ok(()),
                            Err(_) => {
                                let update_req = PutResource::new(&event_path).update(
                                    body_clone,
                                    "text/calendar; charset=utf-8",
                                    "",
                                );
                                if c.request(update_req).await.is_err() {
                                    Err(())
                                } else {
                                    Ok(())
                                }
                            }
                        }
                    }));
                }

                for obsolete_filename in task_existing_filenames {
                    let event_path = format!("{}{}", strip_host(&cal_path), obsolete_filename);
                    let c = client.clone();
                    futures.push(Box::pin(async move {
                        match c.request(Delete::new(&event_path).force()).await {
                            Ok(_) => Ok(()),
                            Err(_) => Ok(()),
                        }
                    }));
                }

                // Fallback for legacy events
                let static_suffixes = ["", "-start", "-due"];
                for suffix in static_suffixes {
                    let event_filename = format!("{}{}.ics", base_uid, suffix);
                    if !generated_events.iter().any(|(s, _)| s == suffix) {
                        let event_path = format!("{}{}", strip_host(&cal_path), event_filename);
                        let c = client.clone();
                        futures.push(Box::pin(async move {
                            match c.request(Delete::new(&event_path).force()).await {
                                Ok(_) => Ok(()),
                                Err(_) => Ok(()),
                            }
                        }));
                    }
                }
            }

            let mut stream = futures::stream::iter(futures).buffer_unordered(8);
            while let Some(res) = stream.next().await {
                if res.is_ok() {
                    success_count += 1;
                }
            }
        }

        Ok(success_count)
    }

    /// Fast-path for bulk deleting all companion events in a calendar.
    /// Uses a single PROPFIND to locate existing events instead of guessing filenames per-task.
    pub async fn delete_all_companion_events(&self, calendar_href: &str) -> anyhow::Result<usize> {
        if calendar_href.starts_with("local://") {
            return Ok(0);
        }

        let client = self
            .client
            .as_ref()
            .ok_or_else(|| anyhow::anyhow!("Offline"))?;
        let path = strip_host(calendar_href);

        let hrefs = self.get_companion_events(&path, None).await?;
        let count = hrefs.len();

        if count == 0 {
            return Ok(0);
        }

        let futures = hrefs.into_iter().map(|href| {
            let c = client.clone();
            async move {
                let _ = c.request(Delete::new(&strip_host(&href)).force()).await;
            }
        });

        let mut stream = stream::iter(futures).buffer_unordered(8);
        while stream.next().await.is_some() {}

        Ok(count)
    }

    pub(crate) async fn fetch_remote_task(&self, task_href: &str) -> Option<Task> {
        // Test hook injection (tests can override)
        #[cfg(any(test, feature = "test_hooks"))]
        {
            if let Some(h) = TEST_FETCH_REMOTE_HOOK.get()
                && let Some(cb) = &*h.lock().unwrap()
            {
                return cb(task_href);
            }
        }

        if let Some(client) = &self.client {
            let path_href = strip_host(task_href);
            let parent_path = if let Some(idx) = path_href.rfind('/') {
                &path_href[..=idx]
            } else {
                "/"
            };

            let req = GetCalendarResources::new(parent_path).with_hrefs(vec![path_href.clone()]);

            if let Ok(resp) = client.request(req).await
                && let Some(item) = resp.resources.into_iter().next()
                && let Ok(content) = item.content
            {
                return IcsAdapter::from_ics(
                    &content.data,
                    content.etag,
                    item.href,
                    parent_path.to_string(),
                )
                .ok();
            }
        }
        None
    }

    async fn fetch_calendar_tasks_internal(
        &self,
        calendar_href: &str,
        apply_journal: bool,
    ) -> anyhow::Result<Vec<Task>> {
        // Local calendar short-circuit
        if calendar_href.starts_with("local://") {
            let mut tasks = LocalStorage::load_for_href(self.ctx.as_ref(), calendar_href)?;
            if apply_journal {
                Journal::apply_to_tasks(self.ctx.as_ref(), &mut tasks, calendar_href);
            }
            return Ok(tasks);
        }

        // Attempt to load cache and compare tokens
        let (mut cached_tasks, cached_token) =
            Cache::load(self.ctx.as_ref(), calendar_href).unwrap_or((vec![], None));

        if let Some(client) = &self.client {
            let path_href = strip_host(calendar_href);

            // Build pending sets from the in-disk journal (if requested)
            let (pending_deletions, pending_active) = if apply_journal {
                let journal = Journal::load(self.ctx.as_ref());
                let mut dels = HashSet::new();
                let mut active = HashSet::new();
                for action in journal.queue {
                    match action {
                        Action::Delete(t) => {
                            if t.calendar_href == calendar_href {
                                dels.insert(t.uid);
                            }
                        }
                        Action::Move(t, _) => {
                            if t.calendar_href == calendar_href {
                                dels.insert(t.uid.clone());
                            }
                            active.insert(t.uid);
                        }
                        Action::Create(t) | Action::Update(t) => {
                            active.insert(t.uid);
                        }
                    }
                }
                (dels, active)
            } else {
                (HashSet::new(), HashSet::new())
            };

            // Fetch remote sync token
            let remote_token = if let Ok(resp) = client
                .request(GetProperty::new(&path_href, &GET_CTAG))
                .await
            {
                resp.value
            } else if let Ok(resp) = client
                .request(GetProperty::new(&path_href, &names::SYNC_TOKEN))
                .await
            {
                resp.value
            } else {
                None
            };

            // Fast-path: if tokens match and there are no unsynced "ghosts"
            let has_ghosts = cached_tasks
                .iter()
                .any(|t| t.etag.is_empty() && !t.href.is_empty());
            if !has_ghosts
                && let (Some(r_tok), Some(c_tok)) = (&remote_token, &cached_token)
                && r_tok == c_tok
            {
                if apply_journal {
                    Journal::apply_to_tasks(self.ctx.as_ref(), &mut cached_tasks, calendar_href);
                }
                return Ok(cached_tasks);
            }

            // Otherwise, enumerate & multiget as needed
            let list_resp = client
                .request(ListResources::new(&path_href))
                .await
                .map_err(|e| anyhow::anyhow!("PROPFIND: {:?}", e))?;

            let mut cache_map: HashMap<String, Task> = HashMap::new();
            for t in cached_tasks {
                cache_map.insert(strip_host(&t.href), t);
            }

            let mut final_tasks = Vec::new();
            let mut to_fetch = Vec::new();
            let mut server_hrefs = HashSet::new();

            for resource in list_resp.resources {
                if !resource.href.ends_with(".ics") {
                    continue;
                }

                let res_href_stripped = strip_host(&resource.href);

                // --- FIX 1: Ignore VEVENT companions during Task sync to stop Multiget spam ---
                let filename = res_href_stripped.split('/').next_back().unwrap_or("");
                if filename.starts_with("evt-") && filename.len() >= 40 {
                    continue;
                }
                // ------------------------------------------------------------------------------

                let should_skip = if let Some(cached) = cache_map.get(&res_href_stripped) {
                    pending_deletions.contains(&cached.uid)
                } else {
                    false
                };

                if should_skip {
                    cache_map.remove(&res_href_stripped);
                    continue;
                }

                server_hrefs.insert(res_href_stripped.clone());
                let remote_etag = resource.etag;

                if let Some(local_task) = cache_map.remove(&res_href_stripped) {
                    if let Some(r_etag) = &remote_etag {
                        if !r_etag.is_empty() && *r_etag == local_task.etag {
                            final_tasks.push(local_task);
                        } else {
                            to_fetch.push(res_href_stripped);
                        }
                    } else {
                        to_fetch.push(res_href_stripped);
                    }
                } else {
                    to_fetch.push(res_href_stripped);
                }
            }

            for (_href, task) in cache_map {
                let is_unsynced = task.etag.is_empty() || task.href.is_empty();
                if is_unsynced {
                    if apply_journal && !pending_active.contains(&task.uid) {
                        continue;
                    }
                    final_tasks.push(task);
                }
            }

            if !to_fetch.is_empty() {
                // Attempt Fast Path first (Concurrency 4, Chunk 100)
                // If it fails, fallback to Safe Path (Concurrency 1, Chunk 50)
                let mut success = false;
                let fetch_attempts = vec![(4, 100), (1, 50)];

                for (concurrency, chunk_size) in fetch_attempts {
                    let chunks: Vec<Vec<String>> =
                        to_fetch.chunks(chunk_size).map(|c| c.to_vec()).collect();
                    let futures = chunks.into_iter().map(|chunk| {
                        let c = client.clone();
                        let p = path_href.clone();
                        async move {
                            c.request(GetCalendarResources::new(&p).with_hrefs(chunk))
                                .await
                        }
                    });

                    let mut stream = stream::iter(futures).buffer_unordered(concurrency);
                    let mut batch_results = Vec::new();
                    let mut batch_error = false;

                    while let Some(res) = stream.next().await {
                        match res {
                            Ok(fetched_resp) => batch_results.push(fetched_resp),
                            Err(_) => {
                                batch_error = true;
                                break;
                            }
                        }
                    }

                    if !batch_error {
                        for fetched_resp in batch_results {
                            for item in fetched_resp.resources {
                                if let Ok(content) = item.content
                                    && let Ok(task) = IcsAdapter::from_ics(
                                        &content.data,
                                        content.etag,
                                        item.href,
                                        calendar_href.to_string(),
                                    )
                                {
                                    if apply_journal && pending_deletions.contains(&task.uid) {
                                        continue;
                                    }
                                    final_tasks.push(task);
                                }
                            }
                        }
                        success = true;
                        break; // Fast path worked, exit loop
                    }
                    // If batch_error is true, the loop continues to the next (safer) attempt
                }

                if !success {
                    return Err(anyhow::anyhow!("Server failed to process task requests."));
                }
            }

            if apply_journal {
                Journal::apply_to_tasks(self.ctx.as_ref(), &mut final_tasks, calendar_href);
            }
            let _ = Cache::save(self.ctx.as_ref(), calendar_href, &final_tasks, remote_token);
            Ok(final_tasks)
        } else {
            if apply_journal {
                Journal::apply_to_tasks(self.ctx.as_ref(), &mut cached_tasks, calendar_href);
            }
            Ok(cached_tasks)
        }
    }

    // --- High-level public APIs that UIs call ---
    // These functions push to the Journal for remote calendars (or write local storage
    // for local calendars), then trigger `sync_journal()` which is implemented in the
    // dedicated sync module. Keeping the optimistic in-memory store updates should
    // happen at the Store/Controller layer (higher up) — these functions perform the
    // persistence/journaling/network steps.

    pub async fn get_tasks(&self, calendar_href: &str) -> anyhow::Result<Vec<Task>> {
        // If sync fails or times out, don't proceed to a full fetch. Fall back to cache + journal.
        // A timeout ensures a slow or rate-limited server doesn't hang the UI startup.
        let sync_res =
            tokio::time::timeout(std::time::Duration::from_secs(10), self.sync_journal()).await;
        if sync_res.is_err() || sync_res.unwrap().is_err() {
            if calendar_href.starts_with("local://") {
                let mut tasks =
                    crate::storage::LocalStorage::load_for_href(self.ctx.as_ref(), calendar_href)?;
                crate::journal::Journal::apply_to_tasks(
                    self.ctx.as_ref(),
                    &mut tasks,
                    calendar_href,
                );
                return Ok(tasks);
            } else {
                let (mut tasks, _) = crate::cache::Cache::load(self.ctx.as_ref(), calendar_href)?;
                crate::journal::Journal::apply_to_tasks(
                    self.ctx.as_ref(),
                    &mut tasks,
                    calendar_href,
                );
                return Ok(tasks);
            }
        }
        // If sync succeeded (or was a no-op), proceed with the network fetch.
        self.fetch_calendar_tasks_internal(calendar_href, true)
            .await
    }

    pub async fn get_all_tasks(
        &self,
        calendars: &[CalendarListEntry],
    ) -> anyhow::Result<Vec<(String, Vec<Task>)>> {
        let _ = tokio::time::timeout(std::time::Duration::from_secs(10), self.sync_journal()).await;
        let hrefs: Vec<String> = calendars.iter().map(|c| c.href.clone()).collect();
        let futures = hrefs.into_iter().map(|href| {
            let client = self.clone();
            async move {
                (
                    href.clone(),
                    client.fetch_calendar_tasks_internal(&href, true).await,
                )
            }
        });

        let mut stream = stream::iter(futures).buffer_unordered(4);
        let mut final_results = Vec::new();

        while let Some((href, res)) = stream.next().await {
            if let Ok(tasks) = res {
                final_results.push((href, tasks));
            }
        }

        Ok(final_results)
    }

    // Removed: create_task moved to TaskController.
    // The RustyClient is now a dumb network layer and should not perform high-level
    // create/update/delete/move operations. Use `TaskController::create_task` for
    // CRUD operations which will handle journaling, local storage and background sync.

    // Removed: update_task moved to TaskController.
    // The RustyClient is now a dumb network layer and should not perform high-level
    // create/update/delete/move operations. Use `TaskController::update_task` for
    // CRUD operations which will handle journaling, local storage and background sync.

    // Removed: delete_task moved to TaskController.
    // The RustyClient is now a dumb network layer and should not perform high-level
    // create/update/delete/move operations. Use `TaskController::delete_task` for
    // CRUD operations which will handle journaling, local storage and background sync.

    // Removed: move_task moved to TaskController.
    // The RustyClient is now a dumb network layer and should not perform high-level
    // create/update/delete/move operations. Use `TaskController::move_task` for
    // CRUD operations which will handle journaling, local storage and background sync.

    pub async fn migrate_tasks(
        &self,
        tasks: Vec<Task>,
        target_calendar_href: &str,
    ) -> anyhow::Result<usize> {
        let mut count = 0;
        for task in tasks.into_iter() {
            let is_source_local = task.calendar_href.starts_with("local://");
            let is_target_local = target_calendar_href.starts_with("local://");

            if is_source_local && !is_target_local {
                let _ =
                    LocalStorage::modify_for_href(self.ctx.as_ref(), &task.calendar_href, |all| {
                        all.retain(|t| t.uid != task.uid);
                    });
                let mut new_task = task.clone();
                new_task.calendar_href = target_calendar_href.to_string();
                new_task.href = String::new();
                new_task.etag = String::new();
                Journal::push(self.ctx.as_ref(), Action::Create(new_task))?;
                count += 1;
            } else if !is_source_local && is_target_local {
                Journal::push(self.ctx.as_ref(), Action::Delete(task.clone()))?;
                let mut new_task = task.clone();
                new_task.calendar_href = target_calendar_href.to_string();
                new_task.href = String::new();
                new_task.etag = String::new();
                let _ =
                    LocalStorage::modify_for_href(self.ctx.as_ref(), target_calendar_href, |all| {
                        all.push(new_task);
                    });
                count += 1;
            } else if is_source_local && is_target_local {
                let _ =
                    LocalStorage::modify_for_href(self.ctx.as_ref(), &task.calendar_href, |all| {
                        all.retain(|t| t.uid != task.uid);
                    });
                let mut new_task = task.clone();
                new_task.calendar_href = target_calendar_href.to_string();
                let _ =
                    LocalStorage::modify_for_href(self.ctx.as_ref(), target_calendar_href, |all| {
                        all.push(new_task);
                    });
                count += 1;
            } else {
                Journal::push(
                    self.ctx.as_ref(),
                    Action::Move(task, target_calendar_href.to_string()),
                )?;
                count += 1;
            }
        }

        match self.sync_journal().await {
            Ok((_warns, _synced)) => Ok(count),
            Err(e) => Err(anyhow::anyhow!(e)),
        }
    }

    pub(crate) async fn fetch_etag(&self, path: &str) -> Option<String> {
        if let Some(client) = &self.client
            && let Ok(resp) = client
                .request(GetProperty::new(path, &names::GETETAG))
                .await
        {
            return resp.value;
        }
        None
    }

    pub async fn create_calendar(&self, name: &str, color: Option<&str>) -> anyhow::Result<String> {
        let client = self
            .client
            .as_ref()
            .ok_or_else(|| anyhow::anyhow!("Offline"))?;

        let principal_res = client.find_current_user_principal().await?;
        let principal = principal_res.ok_or_else(|| anyhow::anyhow!("No principal found"))?;
        let home_set_resp = client
            .request(libdav::caldav::FindCalendarHomeSet::new(principal.path()))
            .await?;
        let home_url = home_set_resp
            .home_sets
            .first()
            .ok_or_else(|| anyhow::anyhow!("No home set found"))?;

        let new_uuid = uuid::Uuid::new_v4().to_string();
        let home_path = home_url.path();
        let new_path = if home_path.ends_with('/') {
            format!("{}{}/", home_path, new_uuid)
        } else {
            format!("{}/{}/", home_path, new_uuid)
        };

        let mut color_xml = String::new();
        if let Some(c) = color {
            color_xml = format!(
                r#"<IC:calendar-color xmlns:IC="http://apple.com/ns/ical/">{}</IC:calendar-color>"#,
                xml_escape(c)
            );
        }

        let body = format!(
            r#"<?xml version="1.0" encoding="utf-8" ?>
<C:mkcalendar xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
  <D:set>
    <D:prop>
      <D:displayname>{}</D:displayname>
      <C:supported-calendar-component-set>
        <C:comp name="VTODO"/>
        <C:comp name="VEVENT"/>
        <C:comp name="VJOURNAL"/>
      </C:supported-calendar-component-set>
      {}
    </D:prop>
  </D:set>
</C:mkcalendar>"#,
            xml_escape(name),
            color_xml
        );

        let req = http::Request::builder()
            .method("MKCALENDAR")
            .uri(client.webdav_client.relative_uri(&new_path)?)
            .header("Content-Type", "application/xml; charset=utf-8")
            .body(body)?;

        let (parts, body_bytes) = client.webdav_client.request_raw(req).await?;
        if parts.status.is_success() {
            Ok(new_path)
        } else {
            let err_body = String::from_utf8_lossy(&body_bytes);
            Err(anyhow::anyhow!(
                "MKCALENDAR failed: {} - {}",
                parts.status,
                err_body
            ))
        }
    }

    pub async fn update_calendar(
        &self,
        href: &str,
        name: &str,
        color: Option<&str>,
    ) -> anyhow::Result<()> {
        let client = self
            .client
            .as_ref()
            .ok_or_else(|| anyhow::anyhow!("Offline"))?;

        let color_set = if let Some(c) = color {
            format!(
                r#"<IC:calendar-color xmlns:IC="http://apple.com/ns/ical/">{}</IC:calendar-color>"#,
                xml_escape(c)
            )
        } else {
            String::new()
        };

        let color_remove = if color.is_none() {
            r#"<D:remove>
    <D:prop>
      <IC:calendar-color xmlns:IC="http://apple.com/ns/ical/"/>
    </D:prop>
  </D:remove>"#
        } else {
            ""
        };

        let body = format!(
            r#"<?xml version="1.0" encoding="utf-8" ?>
<D:propertyupdate xmlns:D="DAV:" xmlns:IC="http://apple.com/ns/ical/">
  <D:set>
    <D:prop>
      <D:displayname>{}</D:displayname>
      {}
    </D:prop>
  </D:set>
  {}
</D:propertyupdate>"#,
            xml_escape(name),
            color_set,
            color_remove
        );

        let req = http::Request::builder()
            .method("PROPPATCH")
            .uri(client.webdav_client.relative_uri(&strip_host(href))?)
            .header("Content-Type", "application/xml; charset=utf-8")
            .body(body)?;

        let (parts, body_bytes) = client.webdav_client.request_raw(req).await?;
        if parts.status.is_success() || parts.status == http::StatusCode::MULTI_STATUS {
            Ok(())
        } else {
            let err_body = String::from_utf8_lossy(&body_bytes);
            Err(anyhow::anyhow!(
                "PROPPATCH failed: {} - {}",
                parts.status,
                err_body
            ))
        }
    }

    // Note: The following methods are implemented in src/client/sync.rs:
    // - handle_create, handle_update, handle_delete, handle_move
    // - sync_journal
    // - attempt_conflict_resolution
    // - execute_move
    //
    // They remain part of the RustyClient impl but are defined in the dedicated
    // sync module to avoid duplication and to keep the synchronization logic
    // consolidated.
}
