//! Runtime-neutral client configuration, composition, and lifecycle ownership.

use crate::{
    AgentSettings, AssetCacheSettings, ConnectionSettings, LoggingSettings, PacketSettings,
    ParcelSettings, TexturePipelineSettings, TimingSettings, WorldSettings,
};
use libremetaverse_types::compat::{CancellationToken, CancellationTokenSource, TimeProvider};
use std::fmt;
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::{Arc, Condvar, Mutex};

/// A validated client-core failure with no credential or capability payload.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum ClientCoreError {
    /// A public settings field failed validation.
    InvalidConfiguration {
        field: &'static str,
        reason: &'static str,
    },
    /// A lifecycle operation was requested in an incompatible state.
    InvalidLifecycle {
        operation: &'static str,
        state: ClientLifecycleState,
    },
    /// An owned service could not complete orderly shutdown.
    ServiceShutdown { service: &'static str },
}

impl fmt::Display for ClientCoreError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::InvalidConfiguration { field, reason } => {
                write!(formatter, "invalid client setting {field}: {reason}")
            }
            Self::InvalidLifecycle { operation, state } => {
                write!(formatter, "cannot {operation} while client is {state:?}")
            }
            Self::ServiceShutdown { service } => {
                write!(formatter, "client service {service} failed to shut down")
            }
        }
    }
}

impl std::error::Error for ClientCoreError {}

impl From<ClientCoreError> for crate::Error {
    fn from(error: ClientCoreError) -> Self {
        match error {
            ClientCoreError::InvalidConfiguration { .. } => Self::Argument,
            ClientCoreError::InvalidLifecycle { .. } | ClientCoreError::ServiceShutdown { .. } => {
                Self::InvalidOperation
            }
        }
    }
}

/// Observable lifecycle of a [`GridClient`].
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[repr(u8)]
pub enum ClientLifecycleState {
    Active,
    ShuttingDown,
    Disposed,
}

impl ClientLifecycleState {
    fn from_u8(value: u8) -> Self {
        match value {
            0 => Self::Active,
            1 => Self::ShuttingDown,
            _ => Self::Disposed,
        }
    }
}

/// Defines the C#-compatible shutdown order for explicitly composed services.
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
#[repr(u8)]
pub enum ShutdownPhase {
    Network = 0,
    Manager = 1,
    Http = 2,
    RateLimiter = 3,
}

/// A client-owned subsystem that participates in deterministic teardown.
///
/// Implementations own their tasks and I/O resources. They must use the client
/// cancellation token and must not require `GridClient` to create an executor.
pub trait ClientService: Send + Sync {
    /// Stable diagnostic name. It must not contain credentials or capability URLs.
    fn name(&self) -> &'static str;

    fn shutdown_phase(&self) -> ShutdownPhase {
        ShutdownPhase::Manager
    }

    /// Cancels and joins resources owned by this service.
    ///
    /// # Errors
    ///
    /// Returns a redacted service error when orderly teardown cannot complete.
    fn shutdown(&self) -> Result<(), ClientCoreError>;
}

struct ServiceRegistration {
    insertion_order: usize,
    service: Arc<dyn ClientService>,
}

struct ClientRuntime {
    state: AtomicU8,
    cancellation: CancellationTokenSource,
    services: Mutex<Vec<ServiceRegistration>>,
    agent_throttle_sender: Mutex<Option<Arc<dyn crate::udp_transport::AgentThrottleSender>>>,
    http_caps_client: Mutex<crate::caps_http::HttpCapsClient>,
    caps_rate_limiter: Mutex<crate::caps_http::CapsRateLimiter>,
    utilization_statistics: Mutex<crate::stats::UtilizationStatistics>,
    throttle: Mutex<crate::AgentThrottle>,
    network_manager: Mutex<std::sync::Weak<crate::network_manager::NetworkManagerInner>>,
    agent_manager: Mutex<std::sync::Weak<crate::agent_manager::AgentManagerInner>>,
    object_manager: Mutex<Option<Arc<crate::object_manager::ObjectManagerInner>>>,
    environment_manager: Mutex<Option<Arc<crate::environment_manager::EnvironmentManagerInner>>>,
    estate_tools: Mutex<Option<Arc<crate::estate_tools::EstateToolsInner>>>,
    terrain_manager: Mutex<Option<Arc<crate::terrain_manager::TerrainManagerInner>>>,
    sound_manager: Mutex<Option<Arc<crate::sound_manager::SoundManagerInner>>>,
    parcel_manager: Mutex<Option<Arc<crate::parcel_manager::ParcelManagerInner>>>,
    grid_manager: Mutex<Option<Arc<crate::grid_manager::GridManagerInner>>>,
    directory_manager: Mutex<Option<Arc<crate::directory_manager::DirectoryManagerInner>>>,
    interest_list_manager: Mutex<Option<Arc<crate::interest_list::InterestListManagerInner>>>,
    friends_manager: Mutex<Option<Arc<crate::friends_manager::FriendsManagerInner>>>,
    group_manager: Mutex<Option<Arc<crate::group_manager::GroupManagerInner>>>,
    marketplace_manager: Mutex<Option<crate::marketplace_runtime::MarketplaceManager>>,
    appearance_manager: Mutex<Option<Arc<crate::appearance_manager::AppearanceManagerInner>>>,
    avatar_manager: Mutex<Option<Arc<crate::avatar_manager::AvatarManagerInner>>>,
    animesh_manager: Mutex<Option<Arc<crate::animesh_runtime::AnimeshManagerInner>>>,
    inventory_manager: Mutex<Option<Arc<crate::inventory_manager::InventoryManagerInner>>>,
    inventory_ais_client: Mutex<Option<crate::inventory_ais::InventoryAISClient>>,
    asset_manager: Mutex<Option<Arc<crate::asset_manager::AssetManagerInner>>>,
    shutdown_complete: Condvar,
    shutdown_wait: Mutex<()>,
}

fn clear_cached<T>(slot: &Mutex<Option<T>>) {
    slot.lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner)
        .take();
}

impl ClientRuntime {
    fn new(
        services: Vec<Arc<dyn ClientService>>,
        http_caps_client: crate::caps_http::HttpCapsClient,
        caps_rate_limiter: crate::caps_http::CapsRateLimiter,
    ) -> Self {
        Self {
            state: AtomicU8::new(ClientLifecycleState::Active as u8),
            cancellation: CancellationTokenSource::new(),
            services: Mutex::new(
                services
                    .into_iter()
                    .enumerate()
                    .map(|(insertion_order, service)| ServiceRegistration {
                        insertion_order,
                        service,
                    })
                    .collect(),
            ),
            agent_throttle_sender: Mutex::new(None),
            http_caps_client: Mutex::new(http_caps_client),
            caps_rate_limiter: Mutex::new(caps_rate_limiter),
            utilization_statistics: Mutex::new(
                crate::stats::UtilizationStatistics::new()
                    .expect("utilization statistics construction is infallible"),
            ),
            throttle: Mutex::new(crate::AgentThrottle::default()),
            network_manager: Mutex::new(std::sync::Weak::new()),
            agent_manager: Mutex::new(std::sync::Weak::new()),
            object_manager: Mutex::new(None),
            environment_manager: Mutex::new(None),
            estate_tools: Mutex::new(None),
            terrain_manager: Mutex::new(None),
            sound_manager: Mutex::new(None),
            parcel_manager: Mutex::new(None),
            grid_manager: Mutex::new(None),
            directory_manager: Mutex::new(None),
            interest_list_manager: Mutex::new(None),
            friends_manager: Mutex::new(None),
            group_manager: Mutex::new(None),
            marketplace_manager: Mutex::new(None),
            appearance_manager: Mutex::new(None),
            avatar_manager: Mutex::new(None),
            animesh_manager: Mutex::new(None),
            inventory_manager: Mutex::new(None),
            inventory_ais_client: Mutex::new(None),
            asset_manager: Mutex::new(None),
            shutdown_complete: Condvar::new(),
            shutdown_wait: Mutex::new(()),
        }
    }

    fn state(&self) -> ClientLifecycleState {
        ClientLifecycleState::from_u8(self.state.load(Ordering::Acquire))
    }

    fn register(&self, service: Arc<dyn ClientService>) -> Result<(), ClientCoreError> {
        let mut services = self
            .services
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        let state = self.state();
        if state != ClientLifecycleState::Active {
            return Err(ClientCoreError::InvalidLifecycle {
                operation: "register a service",
                state,
            });
        }
        let insertion_order = services.len();
        services.push(ServiceRegistration {
            insertion_order,
            service,
        });
        Ok(())
    }

    fn shutdown(&self) -> Result<(), ClientCoreError> {
        if self
            .state
            .compare_exchange(
                ClientLifecycleState::Active as u8,
                ClientLifecycleState::ShuttingDown as u8,
                Ordering::AcqRel,
                Ordering::Acquire,
            )
            .is_err()
        {
            let mut wait = self
                .shutdown_wait
                .lock()
                .unwrap_or_else(std::sync::PoisonError::into_inner);
            while self.state() == ClientLifecycleState::ShuttingDown {
                wait = self
                    .shutdown_complete
                    .wait(wait)
                    .unwrap_or_else(std::sync::PoisonError::into_inner);
            }
            return Ok(());
        }

        self.cancellation.cancel();
        // Service shutdown is user-controlled code. Move registrations out
        // before invoking it so re-entrant lifecycle calls cannot deadlock on
        // the registry mutex.
        let mut services = std::mem::take(
            &mut *self
                .services
                .lock()
                .unwrap_or_else(std::sync::PoisonError::into_inner),
        );
        services.sort_by_key(|entry| (entry.service.shutdown_phase(), entry.insertion_order));
        let mut first_error = None;
        for entry in &services {
            if let Err(error) = entry.service.shutdown()
                && first_error.is_none()
            {
                first_error = Some(error);
            }
        }
        drop(services);
        clear_cached(&self.agent_throttle_sender);
        clear_cached(&self.inventory_manager);
        clear_cached(&self.inventory_ais_client);
        clear_cached(&self.asset_manager);
        clear_cached(&self.appearance_manager);
        clear_cached(&self.avatar_manager);
        clear_cached(&self.animesh_manager);
        clear_cached(&self.object_manager);
        clear_cached(&self.environment_manager);
        clear_cached(&self.estate_tools);
        clear_cached(&self.terrain_manager);
        clear_cached(&self.sound_manager);
        clear_cached(&self.parcel_manager);
        clear_cached(&self.grid_manager);
        clear_cached(&self.directory_manager);
        clear_cached(&self.interest_list_manager);
        clear_cached(&self.friends_manager);
        clear_cached(&self.group_manager);
        clear_cached(&self.marketplace_manager);
        self.http_caps_client
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .shutdown();
        let _ = self
            .caps_rate_limiter
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .dispose();
        let shutdown_wait = self
            .shutdown_wait
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        self.state
            .store(ClientLifecycleState::Disposed as u8, Ordering::Release);
        self.shutdown_complete.notify_all();
        drop(shutdown_wait);
        first_error.map_or(Ok(()), Err)
    }
}

/// A non-owning route back to a live client runtime.
///
/// Manager caches can hold this handle without creating an `Arc` cycle.  The
/// settings and time provider are immutable client composition data and are
/// cloned only when a manager needs to perform an operation.
#[derive(Clone)]
pub(crate) struct ClientWeakHandle {
    runtime: std::sync::Weak<ClientRuntime>,
    settings: Settings,
    time_provider: TimeProvider,
}

impl ClientWeakHandle {
    pub(crate) fn upgrade(&self) -> Option<GridClient> {
        Some(GridClient {
            settings: self.settings.clone(),
            time_provider: self.time_provider.clone(),
            runtime: self.runtime.upgrade()?,
            network_manager: None,
            agent_manager: None,
        })
    }
}

/// Native owner for the mapped C# `GridClient` lifecycle slice.
///
/// Construction is side-effect free: it creates no runtime, task, socket, or
/// HTTP request. Network-facing services are attached explicitly and are owned
/// until ordered shutdown or drop.
pub struct GridClient {
    pub(crate) settings: Settings,
    pub(crate) time_provider: TimeProvider,
    runtime: Arc<ClientRuntime>,
    network_manager: Option<crate::NetworkManager>,
    agent_manager: Option<Box<crate::agent_manager::AgentManager>>,
}

impl Clone for GridClient {
    fn clone(&self) -> Self {
        Self {
            settings: self.settings.clone(),
            time_provider: self.time_provider.clone(),
            runtime: Arc::clone(&self.runtime),
            network_manager: self.network_manager.clone(),
            agent_manager: None,
        }
    }
}

#[cfg(test)]
pub(crate) struct ClientRetentionProbe(std::sync::Weak<ClientRuntime>);

#[cfg(test)]
impl ClientRetentionProbe {
    pub(crate) fn is_released(&self) -> bool {
        self.0.upgrade().is_none()
    }
}

impl GridClient {
    #[must_use]
    pub fn builder() -> GridClientBuilder {
        GridClientBuilder::default()
    }

    /// Returns a shared settings view without requiring mutable client access.
    #[must_use]
    pub fn settings_ref(&self) -> &Settings {
        &self.settings
    }

    #[must_use]
    pub fn cancellation_token(&self) -> CancellationToken {
        self.runtime.cancellation.token()
    }

    #[must_use]
    pub fn lifecycle_state(&self) -> ClientLifecycleState {
        self.runtime.state()
    }

    /// Adds an explicitly constructed service to this client's ownership tree.
    ///
    /// # Errors
    ///
    /// Registration fails after shutdown begins.
    pub fn register_service(
        &mut self,
        service: Arc<dyn ClientService>,
    ) -> Result<(), ClientCoreError> {
        self.runtime.register(service)
    }

    /// Installs the network-owned callback used by mapped `AgentThrottle.Set`.
    ///
    /// # Errors
    ///
    /// Registration fails after client shutdown begins.
    pub fn set_agent_throttle_sender(
        &mut self,
        sender: Arc<dyn crate::udp_transport::AgentThrottleSender>,
    ) -> Result<(), ClientCoreError> {
        let mut registered = self
            .runtime
            .agent_throttle_sender
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        let state = self.lifecycle_state();
        if state != ClientLifecycleState::Active {
            return Err(ClientCoreError::InvalidLifecycle {
                operation: "register the agent throttle sender",
                state,
            });
        }
        *registered = Some(sender);
        Ok(())
    }

    pub(crate) fn agent_throttle_sender(
        &self,
    ) -> Option<Arc<dyn crate::udp_transport::AgentThrottleSender>> {
        self.runtime
            .agent_throttle_sender
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .clone()
    }

    pub(crate) fn native_http_caps_client(&self) -> crate::caps_http::HttpCapsClient {
        self.runtime
            .http_caps_client
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .clone()
    }

    pub(crate) fn native_set_http_caps_client(&mut self, value: crate::caps_http::HttpCapsClient) {
        *self
            .runtime
            .http_caps_client
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = value;
    }

    pub(crate) fn native_caps_rate_limiter(&self) -> crate::caps_http::CapsRateLimiter {
        self.runtime
            .caps_rate_limiter
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .clone()
    }

    pub(crate) fn native_network(&self) -> Result<crate::NetworkManager, crate::Error> {
        if let Some(manager) = &self.network_manager {
            return Ok(manager.clone());
        }
        let mut cached = self
            .runtime
            .network_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        if let Some(inner) = cached.upgrade() {
            return Ok(crate::network_manager::NetworkManager::native_from_inner(
                inner,
            ));
        }
        let manager = crate::network_manager::NetworkManager::native_new(self.clone())?;
        *cached = manager.native_inner_weak();
        Ok(manager)
    }

    pub(crate) fn native_weak_handle(&self) -> ClientWeakHandle {
        ClientWeakHandle {
            runtime: Arc::downgrade(&self.runtime),
            settings: self.settings.clone(),
            time_provider: self.time_provider.clone(),
        }
    }

    pub(crate) fn native_inventory(&self) -> Result<crate::InventoryManager, crate::Error> {
        let mut cached = self
            .runtime
            .inventory_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        if let Some(inner) = cached.as_ref() {
            return Ok(
                crate::inventory_manager::InventoryManager::native_from_inner(Arc::clone(inner)),
            );
        }
        let manager =
            crate::inventory_manager::InventoryManager::native_new(Some(Arc::new(self.clone())))?;
        *cached = Some(manager.native_inner());
        Ok(manager)
    }

    pub(crate) fn native_appearance(&self) -> Result<crate::AppearanceManager, crate::Error> {
        let mut cached = self
            .runtime
            .appearance_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        if let Some(inner) = cached.as_ref() {
            return Ok(
                crate::appearance_manager::AppearanceManager::native_from_inner(Arc::clone(inner)),
            );
        }
        let manager =
            crate::appearance_manager::AppearanceManager::native_new(Some(Arc::new(self.clone())))?;
        *cached = Some(manager.native_inner());
        Ok(manager)
    }

    #[allow(clippy::needless_pass_by_value)]
    pub(crate) fn native_set_appearance(&mut self, value: crate::AppearanceManager) {
        *self
            .runtime
            .appearance_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.native_inner());
    }

    pub(crate) fn native_assets(&self) -> Result<crate::AssetManager, crate::Error> {
        let mut cached = self
            .runtime
            .asset_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        if let Some(inner) = cached.as_ref() {
            return crate::asset_manager::AssetManager::native_from_inner(Arc::clone(inner));
        }
        let manager = crate::asset_manager::AssetManager::new(Some(self.clone()))?;
        *cached = Some(manager.native_inner());
        Ok(manager)
    }

    pub(crate) fn native_avatars(&self) -> Result<crate::AvatarManager, crate::Error> {
        let mut cached = self
            .runtime
            .avatar_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        if let Some(inner) = cached.as_ref() {
            return Ok(crate::avatar_manager::AvatarManager::native_from_inner(
                Arc::clone(inner),
            ));
        }
        let manager =
            crate::avatar_manager::AvatarManager::native_new(Some(Arc::new(self.clone())))?;
        *cached = Some(manager.native_inner());
        Ok(manager)
    }

    #[allow(clippy::needless_pass_by_value)]
    pub(crate) fn native_set_avatars(&mut self, value: crate::AvatarManager) {
        *self
            .runtime
            .avatar_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.native_inner());
    }

    pub(crate) fn native_animesh(&self) -> Result<crate::animesh::AnimeshManager, crate::Error> {
        let mut cached = self
            .runtime
            .animesh_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        if let Some(inner) = cached.as_ref() {
            return Ok(crate::animesh_runtime::AnimeshManager::native_from_inner(
                Arc::clone(inner),
            ));
        }
        let manager = crate::animesh_runtime::AnimeshManager::native_new(self.clone())?;
        *cached = Some(manager.native_inner());
        Ok(manager)
    }

    #[allow(clippy::needless_pass_by_value)]
    pub(crate) fn native_set_animesh(&mut self, value: crate::animesh::AnimeshManager) {
        *self
            .runtime
            .animesh_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.native_inner());
    }

    #[allow(clippy::needless_pass_by_value)]
    pub(crate) fn native_set_assets(&mut self, value: crate::AssetManager) {
        *self
            .runtime
            .asset_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.native_inner());
    }

    pub(crate) fn native_ais_client(&self) -> Result<crate::InventoryAISClient, crate::Error> {
        let mut cached = self
            .runtime
            .inventory_ais_client
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        if let Some(client) = cached.as_ref() {
            return Ok(client.clone());
        }
        let client =
            crate::inventory_ais::InventoryAISClient::native_new(Some(Arc::new(self.clone())))?;
        *cached = Some(client.clone());
        Ok(client)
    }

    #[allow(clippy::needless_pass_by_value)]
    pub(crate) fn native_set_ais_client(&mut self, value: crate::InventoryAISClient) {
        *self
            .runtime
            .inventory_ais_client
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value);
    }

    #[allow(clippy::needless_pass_by_value)] // The mapped C# property setter owns its value.
    pub(crate) fn native_set_inventory(&mut self, value: crate::InventoryManager) {
        *self
            .runtime
            .inventory_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.native_inner());
    }

    #[allow(clippy::needless_pass_by_value)] // The mapped C# property setter owns its value.
    pub(crate) fn native_set_network(&mut self, value: crate::NetworkManager) {
        *self
            .runtime
            .network_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = value.native_inner_weak();
        self.network_manager = Some(value);
    }

    pub(crate) fn native_self(&mut self) -> &mut crate::agent_manager::AgentManager {
        if self.agent_manager.is_none() {
            let client = Arc::new(self.clone());
            let manager = crate::agent_manager::AgentManager::native_new(Some(client))
                .unwrap_or_else(|_| panic!("failed to construct AgentManager"));
            self.agent_manager = Some(Box::new(manager));
        }
        self.agent_manager.as_deref_mut().expect("agent manager")
    }

    pub(crate) fn native_agent_manager(&self) -> Result<crate::AgentManager, crate::Error> {
        crate::agent_manager::AgentManager::native_new(Some(Arc::new(self.clone())))
    }

    pub(crate) fn cached_agent_manager_inner(
        &self,
    ) -> Option<Arc<crate::agent_manager::AgentManagerInner>> {
        self.runtime
            .agent_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .upgrade()
    }

    pub(crate) fn cache_agent_manager_inner(
        &self,
        inner: &Arc<crate::agent_manager::AgentManagerInner>,
    ) {
        *self
            .runtime
            .agent_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = Arc::downgrade(inner);
    }

    pub(crate) fn cached_object_manager_inner(
        &self,
    ) -> Option<Arc<crate::object_manager::ObjectManagerInner>> {
        self.runtime
            .object_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .clone()
    }

    pub(crate) fn cache_object_manager_inner(
        &self,
        inner: &Arc<crate::object_manager::ObjectManagerInner>,
    ) {
        *self
            .runtime
            .object_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(Arc::clone(inner));
    }

    pub(crate) fn install_object_manager_inner(
        &self,
        candidate: Arc<crate::object_manager::ObjectManagerInner>,
    ) -> Arc<crate::object_manager::ObjectManagerInner> {
        let mut cached = self
            .runtime
            .object_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        if let Some(inner) = cached.as_ref() {
            return Arc::clone(inner);
        }
        *cached = Some(Arc::clone(&candidate));
        candidate
    }

    pub(crate) fn native_objects(&self) -> Result<crate::ObjectManager, crate::Error> {
        if let Some(inner) = self.cached_object_manager_inner() {
            return Ok(crate::ObjectManager { inner });
        }
        crate::object_manager::ObjectManagerInner::new(Some(Arc::new(self.clone())))
    }

    #[allow(clippy::needless_pass_by_value)]
    pub(crate) fn native_set_objects(&self, value: crate::ObjectManager) {
        self.cache_object_manager_inner(&value.inner);
    }

    pub(crate) fn install_environment_manager_inner(
        &self,
        candidate: Arc<crate::environment_manager::EnvironmentManagerInner>,
    ) -> Arc<crate::environment_manager::EnvironmentManagerInner> {
        let mut cached = self
            .runtime
            .environment_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        if let Some(inner) = cached.as_ref() {
            return Arc::clone(inner);
        }
        *cached = Some(Arc::clone(&candidate));
        candidate
    }

    pub(crate) fn native_environment(&self) -> Result<crate::EnvironmentManager, crate::Error> {
        if let Some(inner) = self
            .runtime
            .environment_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .clone()
        {
            return Ok(crate::EnvironmentManager { inner });
        }
        crate::environment_manager::EnvironmentManager::native_new(Arc::new(self.clone()))
    }

    #[allow(clippy::needless_pass_by_value)]
    pub(crate) fn native_set_environment(&self, value: crate::EnvironmentManager) {
        *self
            .runtime
            .environment_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.inner);
    }

    pub(crate) fn install_estate_tools_inner(
        &self,
        candidate: Arc<crate::estate_tools::EstateToolsInner>,
    ) -> Arc<crate::estate_tools::EstateToolsInner> {
        let mut cached = self
            .runtime
            .estate_tools
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        if let Some(inner) = cached.as_ref() {
            return Arc::clone(inner);
        }
        *cached = Some(Arc::clone(&candidate));
        candidate
    }

    pub(crate) fn native_estate(&self) -> Result<crate::EstateTools, crate::Error> {
        if let Some(inner) = self
            .runtime
            .estate_tools
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .clone()
        {
            return Ok(crate::estate_tools::EstateTools::native_from_inner(inner));
        }
        crate::estate_tools::EstateTools::native_new(self.clone())
    }

    #[allow(clippy::needless_pass_by_value)]
    pub(crate) fn native_set_estate(&self, value: crate::EstateTools) {
        *self
            .runtime
            .estate_tools
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.native_inner());
    }

    pub(crate) fn cached_terrain_manager_inner(
        &self,
    ) -> Option<Arc<crate::terrain_manager::TerrainManagerInner>> {
        self.runtime
            .terrain_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .clone()
    }
    pub(crate) fn install_terrain_manager_inner(
        &self,
        candidate: Arc<crate::terrain_manager::TerrainManagerInner>,
    ) -> Arc<crate::terrain_manager::TerrainManagerInner> {
        let mut cached = self
            .runtime
            .terrain_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        if let Some(inner) = cached.as_ref() {
            return Arc::clone(inner);
        }
        *cached = Some(Arc::clone(&candidate));
        candidate
    }
    pub(crate) fn native_terrain(&self) -> Result<crate::TerrainManager, crate::Error> {
        if let Some(inner) = self.cached_terrain_manager_inner() {
            Ok(crate::TerrainManager { inner })
        } else {
            crate::terrain_manager::TerrainManager::native_new(Arc::new(self.clone()))
        }
    }
    pub(crate) fn native_set_terrain(&self, value: crate::TerrainManager) {
        *self
            .runtime
            .terrain_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.inner);
    }
    pub(crate) fn cached_sound_manager_inner(
        &self,
    ) -> Option<Arc<crate::sound_manager::SoundManagerInner>> {
        self.runtime
            .sound_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .clone()
    }
    pub(crate) fn install_sound_manager_inner(
        &self,
        candidate: Arc<crate::sound_manager::SoundManagerInner>,
    ) -> Arc<crate::sound_manager::SoundManagerInner> {
        let mut cached = self
            .runtime
            .sound_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        if let Some(inner) = cached.as_ref() {
            return Arc::clone(inner);
        }
        *cached = Some(Arc::clone(&candidate));
        candidate
    }
    pub(crate) fn native_sound(&self) -> Result<crate::SoundManager, crate::Error> {
        if let Some(inner) = self.cached_sound_manager_inner() {
            Ok(crate::SoundManager { inner })
        } else {
            crate::sound_manager::SoundManager::native_new(Arc::new(self.clone()))
        }
    }
    pub(crate) fn native_set_sound(&self, value: crate::SoundManager) {
        *self
            .runtime
            .sound_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.inner);
    }
    pub(crate) fn cached_parcel_manager_inner(
        &self,
    ) -> Option<Arc<crate::parcel_manager::ParcelManagerInner>> {
        self.runtime
            .parcel_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .clone()
    }
    pub(crate) fn install_parcel_manager_inner(
        &self,
        candidate: Arc<crate::parcel_manager::ParcelManagerInner>,
    ) -> Arc<crate::parcel_manager::ParcelManagerInner> {
        let mut cached = self
            .runtime
            .parcel_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        if let Some(inner) = cached.as_ref() {
            return Arc::clone(inner);
        }
        *cached = Some(Arc::clone(&candidate));
        candidate
    }
    pub(crate) fn native_parcels(&self) -> Result<crate::ParcelManager, crate::Error> {
        if let Some(inner) = self.cached_parcel_manager_inner() {
            Ok(crate::ParcelManager { inner })
        } else {
            crate::parcel_manager::ParcelManager::native_new(Arc::new(self.clone()))
        }
    }
    pub(crate) fn native_set_parcels(&self, value: crate::ParcelManager) {
        *self
            .runtime
            .parcel_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.inner);
    }

    pub(crate) fn cached_grid_manager_inner(
        &self,
    ) -> Option<Arc<crate::grid_manager::GridManagerInner>> {
        self.runtime
            .grid_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .clone()
    }
    pub(crate) fn install_grid_manager_inner(
        &self,
        candidate: Arc<crate::grid_manager::GridManagerInner>,
    ) -> Arc<crate::grid_manager::GridManagerInner> {
        let mut cached = self
            .runtime
            .grid_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        if let Some(inner) = cached.as_ref() {
            return Arc::clone(inner);
        }
        *cached = Some(Arc::clone(&candidate));
        candidate
    }
    pub(crate) fn native_grid(&self) -> Result<crate::GridManager, crate::Error> {
        if let Some(inner) = self.cached_grid_manager_inner() {
            Ok(crate::GridManager { inner })
        } else {
            crate::grid_manager::GridManager::native_new(Arc::new(self.clone()))
        }
    }
    pub(crate) fn native_set_grid(&self, value: crate::GridManager) {
        *self
            .runtime
            .grid_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.inner);
    }
    pub(crate) fn cached_directory_manager_inner(
        &self,
    ) -> Option<Arc<crate::directory_manager::DirectoryManagerInner>> {
        self.runtime
            .directory_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .clone()
    }
    pub(crate) fn install_directory_manager_inner(
        &self,
        candidate: Arc<crate::directory_manager::DirectoryManagerInner>,
    ) -> Arc<crate::directory_manager::DirectoryManagerInner> {
        let mut cached = self
            .runtime
            .directory_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        if let Some(inner) = cached.as_ref() {
            return Arc::clone(inner);
        }
        *cached = Some(Arc::clone(&candidate));
        candidate
    }
    pub(crate) fn native_directory(&self) -> Result<crate::DirectoryManager, crate::Error> {
        if let Some(inner) = self.cached_directory_manager_inner() {
            Ok(crate::DirectoryManager { inner })
        } else {
            crate::directory_manager::DirectoryManager::native_new(Arc::new(self.clone()))
        }
    }
    pub(crate) fn native_set_directory(&self, value: crate::DirectoryManager) {
        *self
            .runtime
            .directory_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.inner);
    }
    pub(crate) fn cached_interest_list_manager_inner(
        &self,
    ) -> Option<Arc<crate::interest_list::InterestListManagerInner>> {
        self.runtime
            .interest_list_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .clone()
    }
    pub(crate) fn install_interest_list_manager_inner(
        &self,
        candidate: Arc<crate::interest_list::InterestListManagerInner>,
    ) -> Arc<crate::interest_list::InterestListManagerInner> {
        let mut cached = self
            .runtime
            .interest_list_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        if let Some(inner) = cached.as_ref() {
            return Arc::clone(inner);
        }
        *cached = Some(Arc::clone(&candidate));
        candidate
    }
    pub(crate) fn native_interest_list(&self) -> Result<crate::InterestListManager, crate::Error> {
        if let Some(inner) = self.cached_interest_list_manager_inner() {
            Ok(crate::InterestListManager { inner })
        } else {
            crate::interest_list::InterestListManager::native_new(Arc::new(self.clone()))
        }
    }
    pub(crate) fn native_set_interest_list(&self, value: crate::InterestListManager) {
        *self
            .runtime
            .interest_list_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.inner);
    }
    pub(crate) fn cached_friends_manager_inner(
        &self,
    ) -> Option<Arc<crate::friends_manager::FriendsManagerInner>> {
        self.runtime
            .friends_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .clone()
    }
    pub(crate) fn install_friends_manager_inner(
        &self,
        candidate: Arc<crate::friends_manager::FriendsManagerInner>,
    ) -> Arc<crate::friends_manager::FriendsManagerInner> {
        let mut cached = self
            .runtime
            .friends_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        if let Some(inner) = cached.as_ref() {
            return Arc::clone(inner);
        }
        *cached = Some(Arc::clone(&candidate));
        candidate
    }
    pub(crate) fn native_friends(&self) -> Result<crate::FriendsManager, crate::Error> {
        if let Some(inner) = self.cached_friends_manager_inner() {
            Ok(crate::FriendsManager { inner })
        } else {
            crate::friends_manager::FriendsManager::native_new(Arc::new(self.clone()))
        }
    }
    pub(crate) fn native_set_friends(&self, value: crate::FriendsManager) {
        *self
            .runtime
            .friends_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.inner);
    }
    pub(crate) fn cached_group_manager_inner(
        &self,
    ) -> Option<Arc<crate::group_manager::GroupManagerInner>> {
        self.runtime
            .group_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .clone()
    }
    pub(crate) fn install_group_manager_inner(
        &self,
        candidate: Arc<crate::group_manager::GroupManagerInner>,
    ) -> Arc<crate::group_manager::GroupManagerInner> {
        let mut cached = self
            .runtime
            .group_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        if let Some(inner) = cached.as_ref() {
            return Arc::clone(inner);
        }
        *cached = Some(Arc::clone(&candidate));
        candidate
    }
    pub(crate) fn native_groups(&self) -> crate::GroupManager {
        if let Some(inner) = self.cached_group_manager_inner() {
            crate::GroupManager { inner }
        } else {
            crate::group_manager::GroupManager::native_new(Arc::new(self.clone()))
        }
    }
    pub(crate) fn native_set_groups(&self, value: crate::GroupManager) {
        *self
            .runtime
            .group_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.inner);
    }

    pub(crate) fn native_marketplace(&self) -> crate::marketplace_runtime::MarketplaceManager {
        let mut cached = self
            .runtime
            .marketplace_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        if let Some(manager) = cached.as_ref() {
            return manager.clone();
        }
        let manager = crate::marketplace_runtime::MarketplaceManager::native_new_client_owned(self);
        *cached = Some(manager.clone());
        manager
    }

    pub(crate) fn native_set_marketplace(
        &self,
        value: crate::marketplace_runtime::MarketplaceManager,
    ) {
        value.native_bind_to_client(self);
        *self
            .runtime
            .marketplace_manager
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value);
    }

    pub(crate) fn native_set_caps_rate_limiter(
        &mut self,
        value: crate::caps_http::CapsRateLimiter,
    ) {
        self.native_http_caps_client()
            .set_rate_limiter(Some(value.clone()));
        *self
            .runtime
            .caps_rate_limiter
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = value;
    }

    pub(crate) fn native_stats(&self) -> crate::stats::UtilizationStatistics {
        self.runtime
            .utilization_statistics
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .clone()
    }

    pub(crate) fn native_set_stats(&self, value: crate::stats::UtilizationStatistics) {
        *self
            .runtime
            .utilization_statistics
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = value;
    }

    pub(crate) fn native_throttle(&self) -> crate::AgentThrottle {
        self.runtime
            .throttle
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .clone()
    }

    pub(crate) fn native_set_throttle(&self, value: crate::AgentThrottle) {
        *self
            .runtime
            .throttle
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = value;
    }

    pub(crate) fn shutdown(&self) -> Result<(), ClientCoreError> {
        self.runtime.shutdown()
    }

    #[cfg(test)]
    pub(crate) fn retention_probe(&self) -> ClientRetentionProbe {
        ClientRetentionProbe(Arc::downgrade(&self.runtime))
    }
}

impl fmt::Debug for GridClient {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        let service_names: Vec<_> = self
            .runtime
            .services
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .iter()
            .map(|entry| entry.service.name())
            .collect();
        formatter
            .debug_struct("GridClient")
            .field("settings", &self.settings)
            .field("lifecycle_state", &self.lifecycle_state())
            .field("services", &service_names)
            .field("time_provider", &self.time_provider)
            .finish_non_exhaustive()
    }
}

impl Drop for GridClient {
    fn drop(&mut self) {
        if Arc::strong_count(&self.runtime) == 1 {
            let _ = self.runtime.shutdown();
        }
    }
}

impl crate::IGridClient for GridClient {
    fn ais_client(&self) -> crate::InventoryAISClient {
        self.native_ais_client()
            .unwrap_or_else(|_| panic!("failed to construct InventoryAISClient"))
    }

    fn appearance(&self) -> crate::AppearanceManager {
        self.native_appearance()
            .unwrap_or_else(|_| panic!("failed to construct AppearanceManager"))
    }

    fn assets(&self) -> crate::AssetManager {
        self.native_assets()
            .unwrap_or_else(|_| panic!("failed to construct AssetManager"))
    }

    fn avatars(&self) -> crate::AvatarManager {
        self.native_avatars()
            .unwrap_or_else(|_| panic!("failed to construct AvatarManager"))
    }

    fn directory(&self) -> crate::DirectoryManager {
        self.native_directory()
            .unwrap_or_else(|_| panic!("failed to construct DirectoryManager"))
    }

    fn environment(&self) -> crate::EnvironmentManager {
        self.native_environment()
            .unwrap_or_else(|_| panic!("failed to construct EnvironmentManager"))
    }

    fn estate(&self) -> crate::EstateTools {
        self.native_estate()
            .unwrap_or_else(|_| panic!("failed to construct EstateTools"))
    }

    fn friends(&self) -> crate::FriendsManager {
        self.native_friends()
            .unwrap_or_else(|_| panic!("failed to construct FriendsManager"))
    }

    fn grid(&self) -> crate::GridManager {
        self.native_grid()
            .unwrap_or_else(|_| panic!("failed to construct GridManager"))
    }

    fn groups(&self) -> crate::GroupManager {
        self.native_groups()
    }

    fn http_caps_client(&self) -> crate::caps_http::HttpCapsClient {
        self.native_http_caps_client()
    }

    fn interest_list(&self) -> crate::InterestListManager {
        self.native_interest_list()
            .unwrap_or_else(|_| panic!("failed to construct InterestListManager"))
    }

    fn inventory(&self) -> crate::InventoryManager {
        self.native_inventory()
            .unwrap_or_else(|_| panic!("failed to construct InventoryManager"))
    }

    fn marketplace(&self) -> crate::marketplace::MarketplaceManager {
        self.native_marketplace()
    }

    fn network(&self) -> crate::NetworkManager {
        self.native_network()
            .unwrap_or_else(|_| panic!("failed to construct NetworkManager"))
    }

    fn objects(&self) -> crate::ObjectManager {
        self.native_objects()
            .unwrap_or_else(|_| panic!("failed to construct ObjectManager"))
    }

    fn parcels(&self) -> crate::ParcelManager {
        self.native_parcels()
            .unwrap_or_else(|_| panic!("failed to construct ParcelManager"))
    }

    fn self_(&self) -> crate::AgentManager {
        self.native_agent_manager()
            .unwrap_or_else(|_| panic!("failed to construct AgentManager"))
    }

    fn settings(&self) -> Settings {
        self.settings.clone()
    }

    fn sound(&self) -> crate::SoundManager {
        self.native_sound()
            .unwrap_or_else(|_| panic!("failed to construct SoundManager"))
    }

    fn stats(&self) -> crate::stats::UtilizationStatistics {
        self.native_stats()
    }

    fn terrain(&self) -> crate::TerrainManager {
        self.native_terrain()
            .unwrap_or_else(|_| panic!("failed to construct TerrainManager"))
    }

    fn throttle(&self) -> crate::AgentThrottle {
        self.native_throttle()
    }

    fn time_provider(&self) -> TimeProvider {
        self.time_provider.clone()
    }

    fn set_time_provider(&mut self, value: TimeProvider) {
        self.time_provider = value;
    }
}

/// Explicit, runtime-neutral composition entry point for [`GridClient`].
pub struct GridClientBuilder {
    settings: Settings,
    time_provider: TimeProvider,
    services: Vec<Arc<dyn ClientService>>,
}

impl Default for GridClientBuilder {
    fn default() -> Self {
        Self {
            settings: Settings::defaults(),
            time_provider: TimeProvider::system(),
            services: Vec::new(),
        }
    }
}

impl GridClientBuilder {
    #[must_use]
    pub fn with_settings(mut self, settings: Settings) -> Self {
        self.settings = settings;
        self
    }

    #[must_use]
    pub fn with_time_provider(mut self, time_provider: TimeProvider) -> Self {
        self.time_provider = time_provider;
        self
    }

    #[must_use]
    pub fn with_service(mut self, service: Arc<dyn ClientService>) -> Self {
        self.services.push(service);
        self
    }

    /// Validates configuration and creates a client without starting I/O.
    ///
    /// # Errors
    ///
    /// Returns the first invalid configuration field.
    pub fn build(self) -> Result<GridClient, ClientCoreError> {
        self.settings.validate()?;
        let caps_rate_limiter = crate::caps_http::CapsRateLimiter::new_with_clock_and_overrides(
            self.time_provider.clone(),
            None,
        )
        .map_err(|_| ClientCoreError::InvalidConfiguration {
            field: "CapsRateLimiter",
            reason: "could not construct the configured token buckets",
        })?;
        let max_connections = usize::try_from(Settings::max_http_connections()).map_err(|_| {
            ClientCoreError::InvalidConfiguration {
                field: "Settings.MaxHttpConnections",
                reason: "must be positive",
            }
        })?;
        let caps_timeout = u64::try_from(self.settings.timing.caps_timeout).map_err(|_| {
            ClientCoreError::InvalidConfiguration {
                field: "Timing.CapsTimeout",
                reason: "must be positive",
            }
        })?;
        let http_caps_client = crate::caps_http::HttpCapsClient::production(
            &Settings::user_agent(),
            std::time::Duration::from_millis(caps_timeout),
            max_connections,
            caps_rate_limiter.clone(),
        )
        .map_err(|_| ClientCoreError::InvalidConfiguration {
            field: "HttpCapsClient",
            reason: "could not construct the HTTP transport",
        })?;
        Ok(GridClient {
            settings: self.settings,
            time_provider: self.time_provider,
            runtime: Arc::new(ClientRuntime::new(
                self.services,
                http_caps_client,
                caps_rate_limiter,
            )),
            network_manager: None,
            agent_manager: None,
        })
    }
}

/// Native storage for the mapped grouped settings object.
#[derive(Clone, PartialEq)]
pub struct Settings {
    pub default_effect_color: libremetaverse_types::Color4,
    pub(crate) connection: ConnectionSettings,
    pub(crate) timing: TimingSettings,
    pub(crate) packets: PacketSettings,
    pub(crate) agent: AgentSettings,
    pub(crate) world: WorldSettings,
    pub(crate) parcel: ParcelSettings,
    pub(crate) asset_cache: AssetCacheSettings,
    pub(crate) texture_pipeline: TexturePipelineSettings,
    pub(crate) logging: LoggingSettings,
    pub(crate) upload_cost: i32,
}

impl Settings {
    pub(crate) fn defaults() -> Self {
        Self {
            default_effect_color: libremetaverse_types::Color4 {
                r: 1.0,
                g: 0.0,
                b: 0.0,
                a: 1.0,
            },
            connection: connection_settings_defaults(),
            timing: timing_settings_defaults(),
            packets: packet_settings_defaults(),
            agent: agent_settings_defaults(),
            world: world_settings_defaults(),
            parcel: parcel_settings_defaults(),
            asset_cache: asset_cache_settings_defaults(),
            texture_pipeline: texture_pipeline_settings_defaults(),
            logging: logging_settings_defaults(),
            upload_cost: 0,
        }
    }

    /// Mutable access to the mapped agent settings for native composition.
    pub fn agent_settings_mut(&mut self) -> &mut AgentSettings {
        &mut self.agent
    }

    #[must_use]
    pub fn connection_mut(&mut self) -> &mut ConnectionSettings {
        &mut self.connection
    }

    #[must_use]
    pub fn packets_mut(&mut self) -> &mut PacketSettings {
        &mut self.packets
    }

    #[must_use]
    pub fn agent_mut(&mut self) -> &mut AgentSettings {
        &mut self.agent
    }

    #[must_use]
    pub fn world_mut(&mut self) -> &mut WorldSettings {
        &mut self.world
    }

    #[must_use]
    pub fn parcel_mut(&mut self) -> &mut ParcelSettings {
        &mut self.parcel
    }

    #[must_use]
    pub fn asset_cache_mut(&mut self) -> &mut AssetCacheSettings {
        &mut self.asset_cache
    }

    #[must_use]
    pub fn texture_pipeline_mut(&mut self) -> &mut TexturePipelineSettings {
        &mut self.texture_pipeline
    }

    #[must_use]
    pub fn logging_mut(&mut self) -> &mut LoggingSettings {
        &mut self.logging
    }

    /// Validates all durations, limits, endpoints, cache, and feature policies.
    ///
    /// # Errors
    ///
    /// Returns the first field that cannot be used safely by the native client.
    pub fn validate(&self) -> Result<(), ClientCoreError> {
        validate_endpoint("Connection.LoginServer", &self.connection.login_server)?;
        for (field, value) in [
            ("Timing.TransferTimeout", self.timing.transfer_timeout),
            ("Timing.TeleportTimeout", self.timing.teleport_timeout),
            ("Timing.LogoutTimeout", self.timing.logout_timeout),
            ("Timing.CapsTimeout", self.timing.caps_timeout),
            ("Timing.LoginTimeout", self.timing.login_timeout),
            ("Timing.ResendTimeout", self.timing.resend_timeout),
            ("Timing.SimulatorTimeout", self.timing.simulator_timeout),
            ("Timing.MapRequestTimeout", self.timing.map_request_timeout),
            (
                "Timing.AgentUpdateInterval",
                self.timing.agent_update_interval,
            ),
            (
                "Timing.InterpolationInterval",
                self.timing.interpolation_interval,
            ),
        ] {
            validate_positive(field, value)?;
        }
        validate_positive("Packets.MaxPendingAcks", self.packets.max_pending_acks)?;
        validate_positive("Packets.StatsQueueSize", self.packets.stats_queue_size)?;
        if self.packets.max_resend_count < 0 {
            return invalid("Packets.MaxResendCount", "must be zero or greater");
        }
        if self.agent.send_updates_regularly && !self.agent.send_updates {
            return invalid("Agent.SendUpdatesRegularly", "requires Agent.SendUpdates");
        }
        if (self.parcel.always_request_acl || self.parcel.always_request_dwell)
            && !self.parcel.track_parcels
        {
            return invalid(
                "Parcel.TrackParcels",
                "must be enabled for automatic parcel requests",
            );
        }
        validate_positive(
            "TexturePipeline.MaxConcurrentDownloads",
            self.texture_pipeline.max_concurrent_downloads,
        )?;
        validate_positive(
            "TexturePipeline.RequestTimeout",
            self.texture_pipeline.request_timeout,
        )?;
        if self.asset_cache.enabled {
            if self.asset_cache.dir.trim().is_empty() {
                return invalid(
                    "AssetCache.Dir",
                    "must not be empty when caching is enabled",
                );
            }
            if self.asset_cache.max_size <= 0 {
                return invalid(
                    "AssetCache.MaxSize",
                    "must be positive when caching is enabled",
                );
            }
        }
        Ok(())
    }
}

impl Default for Settings {
    fn default() -> Self {
        Self::defaults()
    }
}

impl fmt::Debug for Settings {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("Settings")
            .field("default_effect_color", &self.default_effect_color)
            .field("connection", &self.connection)
            .field("timing", &self.timing)
            .field("packets", &self.packets)
            .field("agent", &self.agent)
            .field("world", &self.world)
            .field("parcel", &self.parcel)
            .field("asset_cache", &self.asset_cache)
            .field("texture_pipeline", &self.texture_pipeline)
            .field("logging", &self.logging)
            .field("upload_cost", &self.upload_cost)
            .finish()
    }
}

impl fmt::Debug for ConnectionSettings {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("ConnectionSettings")
            .field("login_server", &"<redacted endpoint>")
            .field("mfa_enabled", &self.mfa_enabled)
            .finish()
    }
}

fn invalid<T>(field: &'static str, reason: &'static str) -> Result<T, ClientCoreError> {
    Err(ClientCoreError::InvalidConfiguration { field, reason })
}

fn validate_positive(field: &'static str, value: i32) -> Result<(), ClientCoreError> {
    if value <= 0 {
        invalid(field, "must be positive")
    } else {
        Ok(())
    }
}

fn validate_endpoint(field: &'static str, endpoint: &str) -> Result<(), ClientCoreError> {
    let endpoint = endpoint.trim();
    let remainder = endpoint
        .strip_prefix("https://")
        .or_else(|| endpoint.strip_prefix("http://"))
        .ok_or(ClientCoreError::InvalidConfiguration {
            field,
            reason: "must use an HTTP or HTTPS URL",
        })?;
    let authority = remainder.split(['/', '?', '#']).next().unwrap_or_default();
    if authority.is_empty() || authority.contains('@') || endpoint.chars().any(char::is_whitespace)
    {
        return invalid(
            field,
            "must contain a host and must not embed credentials or whitespace",
        );
    }
    Ok(())
}

pub(crate) fn agent_settings_defaults() -> AgentSettings {
    AgentSettings {
        disable_update_duplicate_check: true,
        multiple_sims: false,
        send_appearance: true,
        send_throttle: true,
        send_updates: true,
        send_updates_regularly: true,
    }
}

pub(crate) fn asset_cache_settings_defaults() -> AssetCacheSettings {
    AssetCacheSettings {
        dir: std::path::Path::new("linden")
            .join("cache")
            .to_string_lossy()
            .into_owned(),
        enabled: true,
        max_size: 1_024 * 1_024 * 1_024,
    }
}

pub(crate) fn connection_settings_defaults() -> ConnectionSettings {
    ConnectionSettings {
        login_server: Settings::AGNI_LOGIN_SERVER.to_owned(),
        mfa_enabled: false,
    }
}

pub(crate) fn logging_settings_defaults() -> LoggingSettings {
    LoggingSettings {
        log_disk_cache: true,
        log_names: true,
        log_resends: true,
    }
}

pub(crate) fn packet_settings_defaults() -> PacketSettings {
    PacketSettings {
        enable_sim_stats: true,
        max_pending_acks: 10,
        max_resend_count: 3,
        send_pings: true,
        stats_queue_size: 5,
        throttle_outgoing: true,
        track_utilization: false,
    }
}

pub(crate) fn parcel_settings_defaults() -> ParcelSettings {
    ParcelSettings {
        always_request_acl: true,
        always_request_dwell: true,
        pool_parcel_data: false,
        track_parcels: true,
    }
}

pub(crate) fn texture_pipeline_settings_defaults() -> TexturePipelineSettings {
    TexturePipelineSettings {
        enabled: true,
        max_concurrent_downloads: 4,
        request_timeout: 45_000,
        use_http_textures: true,
    }
}

pub(crate) fn timing_settings_defaults() -> TimingSettings {
    TimingSettings {
        agent_update_interval: 500,
        caps_timeout: 60_000,
        interpolation_interval: 250,
        login_timeout: 60_000,
        logout_timeout: 5_000,
        map_request_timeout: 15_000,
        resend_timeout: 4_000,
        simulator_timeout: 30_000,
        teleport_timeout: 40_000,
        transfer_timeout: 90_000,
    }
}

pub(crate) fn world_settings_defaults() -> WorldSettings {
    WorldSettings {
        always_decode_objects: true,
        always_request_objects: true,
        cache_primitives: false,
        store_land_patches: false,
        track_avatars: true,
        track_objects: true,
        use_interpolation_timer: true,
    }
}

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

    struct RecordingService {
        name: &'static str,
        phase: ShutdownPhase,
        order: Arc<Mutex<Vec<&'static str>>>,
        calls: AtomicUsize,
    }

    impl ClientService for RecordingService {
        fn name(&self) -> &'static str {
            self.name
        }

        fn shutdown_phase(&self) -> ShutdownPhase {
            self.phase
        }

        fn shutdown(&self) -> Result<(), ClientCoreError> {
            self.calls.fetch_add(1, Ordering::Relaxed);
            self.order
                .lock()
                .unwrap_or_else(std::sync::PoisonError::into_inner)
                .push(self.name);
            Ok(())
        }
    }

    struct ReentrantService {
        client: Mutex<Option<GridClient>>,
        calls: AtomicUsize,
    }

    impl ClientService for ReentrantService {
        fn name(&self) -> &'static str {
            "reentrant"
        }

        fn shutdown(&self) -> Result<(), ClientCoreError> {
            self.calls.fetch_add(1, Ordering::AcqRel);
            let result = self
                .client
                .lock()
                .unwrap_or_else(std::sync::PoisonError::into_inner)
                .as_mut()
                .expect("client installed")
                .register_service(Arc::new(RecordingService {
                    name: "late",
                    phase: ShutdownPhase::Manager,
                    order: Arc::new(Mutex::new(Vec::new())),
                    calls: AtomicUsize::new(0),
                }));
            assert!(matches!(
                result,
                Err(ClientCoreError::InvalidLifecycle {
                    state: ClientLifecycleState::ShuttingDown,
                    ..
                })
            ));
            Ok(())
        }
    }

    #[test]
    #[allow(clippy::float_cmp)] // Exact representable constants are the compatibility contract.
    fn defaults_match_reference_and_validate() {
        let settings = Settings::defaults();
        assert_eq!(
            settings.connection.login_server,
            Settings::AGNI_LOGIN_SERVER
        );
        assert_eq!(settings.timing.transfer_timeout, 90_000);
        assert_eq!(settings.timing.teleport_timeout, 40_000);
        assert_eq!(settings.timing.logout_timeout, 5_000);
        assert_eq!(settings.timing.caps_timeout, 60_000);
        assert_eq!(settings.timing.login_timeout, 60_000);
        assert_eq!(settings.timing.resend_timeout, 4_000);
        assert_eq!(settings.timing.simulator_timeout, 30_000);
        assert_eq!(settings.timing.map_request_timeout, 15_000);
        assert_eq!(settings.timing.agent_update_interval, 500);
        assert_eq!(settings.timing.interpolation_interval, 250);
        assert_eq!(settings.packets.max_pending_acks, 10);
        assert_eq!(settings.packets.stats_queue_size, 5);
        assert_eq!(settings.packets.max_resend_count, 3);
        assert!(settings.packets.throttle_outgoing);
        assert!(settings.packets.enable_sim_stats);
        assert!(settings.packets.send_pings);
        assert!(!settings.packets.track_utilization);
        assert!(settings.agent.send_updates);
        assert!(settings.agent.send_updates_regularly);
        assert!(settings.agent.send_appearance);
        assert!(settings.agent.send_throttle);
        assert!(settings.agent.disable_update_duplicate_check);
        assert!(!settings.agent.multiple_sims);
        assert!(settings.world.always_decode_objects);
        assert!(settings.world.always_request_objects);
        assert!(settings.world.track_objects);
        assert!(settings.world.track_avatars);
        assert!(!settings.world.cache_primitives);
        assert!(settings.world.use_interpolation_timer);
        assert!(!settings.world.store_land_patches);
        assert!(settings.parcel.track_parcels);
        assert!(settings.parcel.always_request_acl);
        assert!(settings.parcel.always_request_dwell);
        assert!(!settings.parcel.pool_parcel_data);
        assert!(settings.asset_cache.enabled);
        assert!(settings.asset_cache.dir.ends_with("cache"));
        assert_eq!(settings.texture_pipeline.max_concurrent_downloads, 4);
        assert!(settings.texture_pipeline.enabled);
        assert!(settings.texture_pipeline.use_http_textures);
        assert_eq!(settings.texture_pipeline.request_timeout, 45_000);
        assert!(settings.logging.log_names);
        assert!(settings.logging.log_resends);
        assert!(settings.logging.log_disk_cache);
        assert_eq!(settings.asset_cache.max_size, 1_073_741_824);
        assert_eq!(settings.default_effect_color.r, 1.0);
        assert_eq!(settings.upload_cost, 0);
        assert_eq!(settings.validate(), Ok(()));
        assert_eq!(Settings::PING_INTERVAL, 2_200);
        assert_eq!(Settings::NETWORK_TICK_INTERVAL, 500);
        assert_eq!(Settings::MAX_PACKET_SIZE, 1_200);
        assert_eq!(Settings::MAX_SEQUENCE, 0xFF_FFFF);
        assert_eq!(Settings::user_agent(), "LibreMetaverse");
        assert_eq!(Settings::resource_dir(), "linden");
        assert_eq!(Settings::max_http_connections(), 32);
        assert_eq!(Settings::packet_archive_size(), 1_000);
        assert_eq!(Settings::udp_receive_queue_capacity(), 512);
        assert_eq!(Settings::texture_pipeline_refresh_interval(), 500.0);
        assert_eq!(Settings::simulator_pool_timeout(), 120_000);
        assert!(Settings::bind_address().is_unspecified());
    }

    #[test]
    fn validation_reports_the_exact_invalid_field() {
        let mut settings = Settings::defaults();
        settings.timing.login_timeout = 0;
        assert_eq!(
            settings.validate(),
            Err(ClientCoreError::InvalidConfiguration {
                field: "Timing.LoginTimeout",
                reason: "must be positive",
            })
        );

        let mut settings = Settings::defaults();
        settings.connection.login_server = "https://user:secret@example.test/login".to_owned();
        assert!(matches!(
            settings.validate(),
            Err(ClientCoreError::InvalidConfiguration {
                field: "Connection.LoginServer",
                ..
            })
        ));
        assert!(!format!("{settings:?}").contains("secret"));

        let mut settings = Settings::defaults();
        settings.agent.send_updates = false;
        assert_eq!(
            settings.validate(),
            Err(ClientCoreError::InvalidConfiguration {
                field: "Agent.SendUpdatesRegularly",
                reason: "requires Agent.SendUpdates",
            })
        );
    }

    #[test]
    fn construction_is_inert_and_shutdown_is_ordered_and_idempotent() {
        let order = Arc::new(Mutex::new(Vec::new()));
        let manager = Arc::new(RecordingService {
            name: "manager",
            phase: ShutdownPhase::Manager,
            order: Arc::clone(&order),
            calls: AtomicUsize::new(0),
        });
        let network = Arc::new(RecordingService {
            name: "network",
            phase: ShutdownPhase::Network,
            order: Arc::clone(&order),
            calls: AtomicUsize::new(0),
        });
        let client = GridClient::builder()
            .with_service(manager.clone())
            .with_service(network.clone())
            .build()
            .unwrap();

        assert_eq!(client.lifecycle_state(), ClientLifecycleState::Active);
        assert!(!client.cancellation_token().is_cancellation_requested());
        client.shutdown().unwrap();
        client.shutdown().unwrap();
        assert_eq!(client.lifecycle_state(), ClientLifecycleState::Disposed);
        assert!(client.cancellation_token().is_cancellation_requested());
        assert_eq!(
            *order
                .lock()
                .unwrap_or_else(std::sync::PoisonError::into_inner),
            vec!["network", "manager"]
        );
        assert_eq!(network.calls.load(Ordering::Relaxed), 1);
        assert_eq!(manager.calls.load(Ordering::Relaxed), 1);
    }

    #[test]
    fn shutdown_does_not_hold_service_registry_lock_across_callbacks() {
        let service = Arc::new(ReentrantService {
            client: Mutex::new(None),
            calls: AtomicUsize::new(0),
        });
        let client = GridClient::builder()
            .with_service(service.clone())
            .build()
            .unwrap();
        *service
            .client
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(client.clone());

        client.shutdown().unwrap();
        assert_eq!(client.lifecycle_state(), ClientLifecycleState::Disposed);
        assert_eq!(service.calls.load(Ordering::Acquire), 1);
        service
            .client
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .take();
    }
}
