//! Narrow injected boundaries between orchestration and grid/world I/O.

use crate::policy::AuthorizedAction;
use crate::types::{GridEvent, GridEventKind, ToolCallOutcome};
#[cfg(feature = "live-grid")]
use libremetaverse_types::UUID;
use libremetaverse_types::compat::CancellationToken;
#[cfg(feature = "live-grid")]
use std::collections::BTreeMap;
use std::error::Error;
use std::fmt;
use std::future::Future;
use std::pin::Pin;
#[cfg(feature = "live-grid")]
use std::sync::Arc;
#[cfg(feature = "live-grid")]
use std::sync::atomic::{AtomicBool, Ordering};
#[cfg(feature = "live-grid")]
use std::time::Duration;
use tokio::sync::mpsc;

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

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum BackendError {
    Configuration { component: &'static str },
    EventQueueClosed,
    RejectedMutation,
    Operation { operation: &'static str },
}

impl fmt::Display for BackendError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Configuration { component } => {
                write!(formatter, "backend configuration failed for {component}")
            }
            Self::EventQueueClosed => formatter.write_str("grid-event owner closed its queue"),
            Self::RejectedMutation => {
                formatter.write_str("world mutation lacks an approved policy decision")
            }
            Self::Operation { operation } => {
                write!(formatter, "backend operation failed: {operation}")
            }
        }
    }
}

impl Error for BackendError {}

/// One owned grid event source. Implementations send into the coordinator-owned
/// bounded queue and must finish when cancellation is requested.
pub trait GridBackend: Send + Sync + 'static {
    fn name(&self) -> &'static str;

    fn run(
        &self,
        events: mpsc::Sender<GridEvent>,
        cancellation: CancellationToken,
    ) -> BackendFuture<'_, Result<(), BackendError>>;
}

/// Backend for an already authorized tool action. `AuthorizedAction` has no
/// public constructor and binds the exact name, arguments, principal, budget,
/// and one policy authorization.
pub trait AuthorizedToolBackend: Send + Sync + 'static {
    fn apply(
        &self,
        action: AuthorizedAction,
        cancellation: CancellationToken,
    ) -> BackendFuture<'_, Result<ToolCallOutcome, BackendError>>;
}

/// Marker for world-mutating backends. Production mutations use the same
/// opaque authorization boundary as every other tool backend.
pub trait WorldMutator: AuthorizedToolBackend {}

/// Inert deterministic backend used by the foundational offline service.
///
/// It performs no login or network operation and is available without the
/// live-grid dependency graph.
#[derive(Clone, Copy, Debug, Default)]
pub struct OfflineGridBackend;

impl OfflineGridBackend {
    #[must_use]
    pub const fn new() -> Self {
        Self
    }
}

/// Live-feature composition owner that guarantees production adapters reuse
/// the existing `libremetaverse` manager/client graph.
#[cfg(feature = "live-grid")]
pub struct LibremetaverseClientOwner {
    client: libremetaverse::GridClient,
    grid: libremetaverse::GridManager,
    network: libremetaverse::NetworkManager,
    agent: Arc<libremetaverse::AgentManager>,
    delivery_generation: Arc<std::sync::RwLock<Option<u64>>>,
}

/// Read-only adapter over caches already owned and populated by the shared
/// native manager graph. Capture never sends a packet or requests a capability.
#[cfg(feature = "live-grid")]
#[derive(Clone)]
pub struct LibremetaverseWorldSnapshotSource {
    client: libremetaverse::GridClient,
    agent: Arc<libremetaverse::AgentManager>,
    world: metacrate_game_loop::WorldReader,
}

#[cfg(feature = "live-grid")]
#[derive(Clone)]
pub struct LibremetaverseEmbodimentSink {
    client: libremetaverse::GridClient,
    agent: Arc<libremetaverse::AgentManager>,
    generation: Arc<std::sync::RwLock<Option<u64>>>,
}

#[cfg(feature = "live-grid")]
impl fmt::Debug for LibremetaverseClientOwner {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("LibremetaverseClientOwner")
            .field("client", &"[NATIVE CLIENT REDACTED]")
            .finish_non_exhaustive()
    }
}

/// Feature-gated session adapter that reuses the existing native
/// `NetworkManager` login, event, disconnect, and logout lifecycle.
#[cfg(feature = "live-grid")]
#[derive(Clone)]
pub struct LibremetaverseSessionBackend {
    grid: libremetaverse::GridManager,
    network: libremetaverse::NetworkManager,
    agent: Arc<libremetaverse::AgentManager>,
    throttle: libremetaverse::AgentThrottle,
    login_url: String,
    first_name: String,
    last_name: String,
    password: crate::config::SecretString,
    interaction: Option<crate::interaction::InteractionIngress>,
    perception: Option<crate::perception::PerceptionIngress>,
    behavior: Option<crate::behavior::BehaviorIngress>,
    framework_readiness: Option<Arc<crate::session::FrameworkReadiness>>,
    delivery_generation: Arc<std::sync::RwLock<Option<u64>>>,
}

#[cfg(feature = "live-grid")]
impl fmt::Debug for LibremetaverseSessionBackend {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("LibremetaverseSessionBackend")
            .field("transport_connected", &self.network.native_connected())
            .field("login_url", &"[REDACTED ENDPOINT]")
            .field("avatar", &"[REDACTED IDENTITY]")
            .field("password", &self.password)
            .finish_non_exhaustive()
    }
}

#[cfg(feature = "live-grid")]
impl LibremetaverseClientOwner {
    /// Builds the shared client composition root without starting login or I/O.
    ///
    /// # Errors
    ///
    /// Returns a backend configuration error if the shared client defaults are invalid.
    pub fn new() -> Result<Self, BackendError> {
        Self::new_with_asset_cache_max_bytes(2 * 1024 * 1024 * 1024)
    }

    /// Builds the shared client with the configured persistent asset-cache limit.
    ///
    /// # Errors
    ///
    /// Returns a backend configuration error for a zero or unsupported limit.
    pub fn new_with_asset_cache_max_bytes(max_bytes: u64) -> Result<Self, BackendError> {
        let mut settings = libremetaverse::Settings::default();
        settings.asset_cache_mut().dir = metacrate_cache_dir()?.to_string_lossy().into_owned();
        settings.asset_cache_mut().max_size =
            i64::try_from(max_bytes).map_err(|_| BackendError::Configuration {
                component: "persistent asset cache limit",
            })?;
        if settings.asset_cache().max_size == 0 {
            return Err(BackendError::Configuration {
                component: "persistent asset cache limit",
            });
        }
        let client = libremetaverse::GridClientBuilder::default()
            .with_settings(settings)
            .build()
            .map_err(|_| BackendError::Configuration {
                component: "libremetaverse client defaults",
            })?;
        // The compatibility client caches its network manager weakly. Retain the
        // composition root before managers register packet subscriptions on it.
        let network = client.network();
        let grid = client.grid();
        let _terrain = client.terrain();
        let _objects = client.objects();
        let agent = Arc::new(
            libremetaverse::AgentManager::new(Some(Arc::new(client.clone()))).map_err(|_| {
                BackendError::Configuration {
                    component: "native agent manager",
                }
            })?,
        );
        Ok(Self {
            client,
            grid,
            network,
            agent,
            delivery_generation: Arc::new(std::sync::RwLock::new(None)),
        })
    }

    #[must_use]
    pub const fn client(&self) -> &libremetaverse::GridClient {
        &self.client
    }

    /// Returns the one native agent manager owned by this composition root.
    /// `MetaCrate` adapters clone the `Arc`; they never construct a parallel
    /// compatibility manager graph.
    #[must_use]
    pub fn agent(&self) -> Arc<libremetaverse::AgentManager> {
        Arc::clone(&self.agent)
    }

    /// Creates the supervised live-session adapter without logging in.
    ///
    /// # Errors
    ///
    /// Rejects a malformed avatar identity before any network operation.
    pub fn session_backend(
        &self,
        connection: crate::config::GridConnection,
    ) -> Result<LibremetaverseSessionBackend, BackendError> {
        self.session_backend_inner(connection, None, None, None)
    }

    /// Creates a supervised live session whose generation owns exactly one
    /// native chat and IM subscription.
    ///
    /// # Errors
    ///
    /// Rejects a malformed avatar identity before any subscription or login.
    pub fn session_backend_with_interaction(
        &self,
        connection: crate::config::GridConnection,
        ingress: crate::interaction::InteractionIngress,
    ) -> Result<LibremetaverseSessionBackend, BackendError> {
        self.session_backend_inner(connection, Some(ingress), None, None)
    }

    /// Creates a session that generation-fences interaction and perception.
    ///
    /// # Errors
    ///
    /// Rejects a malformed avatar identity before any subscription or login.
    pub fn session_backend_with_services(
        &self,
        connection: crate::config::GridConnection,
        interaction: crate::interaction::InteractionIngress,
        perception: crate::perception::PerceptionIngress,
    ) -> Result<LibremetaverseSessionBackend, BackendError> {
        self.session_backend_inner(connection, Some(interaction), Some(perception), None)
    }

    /// Creates a session that generation-fences interaction, perception, and embodiment.
    ///
    /// # Errors
    ///
    /// Rejects a malformed avatar identity before any subscription or login.
    pub fn session_backend_with_agent_services(
        &self,
        connection: crate::config::GridConnection,
        interaction: crate::interaction::InteractionIngress,
        perception: crate::perception::PerceptionIngress,
        behavior: crate::behavior::BehaviorIngress,
    ) -> Result<LibremetaverseSessionBackend, BackendError> {
        self.session_backend_inner(
            connection,
            Some(interaction),
            Some(perception),
            Some(behavior),
        )
    }

    fn session_backend_inner(
        &self,
        connection: crate::config::GridConnection,
        interaction: Option<crate::interaction::InteractionIngress>,
        perception: Option<crate::perception::PerceptionIngress>,
        behavior: Option<crate::behavior::BehaviorIngress>,
    ) -> Result<LibremetaverseSessionBackend, BackendError> {
        let avatar = connection.avatar_name.trim();
        let (first_name, last_name) = avatar
            .split_once(char::is_whitespace)
            .map_or((avatar, "Resident"), |(first, last)| (first, last.trim()));
        if first_name.is_empty() || last_name.is_empty() {
            return Err(BackendError::Configuration {
                component: "grid avatar identity",
            });
        }
        Ok(LibremetaverseSessionBackend {
            grid: self.grid.clone(),
            network: self.network.clone(),
            agent: Arc::clone(&self.agent),
            throttle: libremetaverse::AgentThrottle::new_with_grid_client(self.client.clone())
                .map_err(|_| BackendError::Configuration {
                    component: "libremetaverse agent throttle",
                })?,
            login_url: connection.login_url.expose_url().to_owned(),
            first_name: first_name.to_owned(),
            last_name: last_name.to_owned(),
            password: connection.password,
            interaction,
            perception,
            behavior,
            framework_readiness: None,
            delivery_generation: Arc::clone(&self.delivery_generation),
        })
    }

    #[must_use]
    pub fn interaction_sink(&self) -> LibremetaverseInteractionSink {
        LibremetaverseInteractionSink {
            agent: Arc::clone(&self.agent),
            delivery_generation: Arc::clone(&self.delivery_generation),
        }
    }

    #[must_use]
    pub fn world_snapshot_source(
        &self,
        world: metacrate_game_loop::WorldReader,
    ) -> LibremetaverseWorldSnapshotSource {
        LibremetaverseWorldSnapshotSource {
            client: self.client.clone(),
            agent: Arc::clone(&self.agent),
            world,
        }
    }

    #[must_use]
    pub fn embodiment_sink(&self) -> LibremetaverseEmbodimentSink {
        LibremetaverseEmbodimentSink {
            client: self.client.clone(),
            agent: Arc::clone(&self.agent),
            generation: Arc::clone(&self.delivery_generation),
        }
    }
}

#[cfg(feature = "live-grid")]
impl LibremetaverseSessionBackend {
    #[must_use]
    pub fn with_framework_readiness(
        mut self,
        readiness: Arc<crate::session::FrameworkReadiness>,
    ) -> Self {
        self.framework_readiness = Some(readiness);
        self
    }
}

#[cfg(feature = "live-grid")]
fn metacrate_cache_dir() -> Result<std::path::PathBuf, BackendError> {
    directories::ProjectDirs::from("", "", "metacrate")
        .map(|directories| directories.cache_dir().to_owned())
        .ok_or(BackendError::Configuration {
            component: "platform cache directory",
        })
}

#[cfg(feature = "live-grid")]
impl LibremetaverseWorldSnapshotSource {
    #[must_use]
    pub fn current_region_handle(&self) -> Option<u64> {
        self.world
            .latest()
            .and_then(|world| world.value.region.as_ref().map(|region| region.handle))
    }

    // One atomic projection keeps native cache reads together. Terrain lookup
    // takes integral local coordinates, so finite region positions are floored.
    #[allow(clippy::too_many_lines, clippy::cast_possible_truncation)]
    fn capture_blocking(
        &self,
        generation: u64,
    ) -> Result<crate::perception::WorldSnapshot, crate::perception::PerceptionError> {
        let network = self.client.network();
        let simulator = network
            .native_current_sim()
            .ok_or(crate::perception::PerceptionError::SourceUnavailable)?;
        if !network.native_connected()
            || !simulator
                .native_is_event_queue_running(None)
                .unwrap_or(false)
        {
            return Err(crate::perception::PerceptionError::NotReady);
        }
        let world = self
            .world
            .latest()
            .ok_or(crate::perception::PerceptionError::NotReady)?;
        if world.value.region.as_ref().map(|region| region.id) != Some(simulator.region_id) {
            return Err(crate::perception::PerceptionError::NotReady);
        }
        let agent_position = self.agent.sim_position();
        let position = crate::perception::WorldPosition {
            x: f64::from(agent_position.x),
            y: f64::from(agent_position.y),
            z: f64::from(agent_position.z),
        };
        let avatars_truncated = world.value.avatars.len() > 2_048;
        let mut avatars = BTreeMap::new();
        for (local_id, avatar) in world
            .value
            .avatars
            .iter()
            .filter(|(_, avatar)| avatar.id != UUID::zero())
        {
            avatars.insert(
                *local_id,
                crate::perception::ObservedAvatar {
                    id: avatar.id,
                    name: crate::perception::sanitize_untrusted(&avatar.name()),
                    position: native_position(avatar.position),
                },
            );
            if avatars.len() > 2_048 {
                avatars.pop_last();
            }
        }
        let avatars = avatars.into_values().collect();
        let objects_truncated = world.value.primitives.len() > 2_048;
        let mut objects = BTreeMap::new();
        for (local_id, primitive) in world
            .value
            .primitives
            .iter()
            .filter(|(_, primitive)| primitive.id != UUID::zero())
        {
            let observed = {
                let (name, description) = primitive.properties.as_ref().map_or_else(
                    || (String::new(), String::new()),
                    |properties| {
                        (
                            crate::perception::sanitize_untrusted(&properties.name),
                            crate::perception::sanitize_untrusted(&properties.description),
                        )
                    },
                );
                crate::perception::ObservedObject {
                    id: primitive.id,
                    local_id: primitive.local_id,
                    name,
                    description,
                    hover_text: crate::perception::sanitize_untrusted(&primitive.text),
                    position: native_position(primitive.position),
                    scale: native_position(primitive.scale),
                    is_attachment: primitive.is_attachment,
                }
            };
            objects.insert(*local_id, observed);
            if objects.len() > 2_048 {
                objects.pop_last();
            }
        }
        let objects = objects.into_values().collect();
        let parcel = simulator
            .parcels
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .values()
            .filter(|parcel| {
                agent_position.x >= parcel.aabb_min.x
                    && agent_position.x <= parcel.aabb_max.x
                    && agent_position.y >= parcel.aabb_min.y
                    && agent_position.y <= parcel.aabb_max.y
            })
            .min_by_key(|parcel| parcel.local_id)
            .map(|parcel| crate::perception::ParcelSnapshot {
                name: crate::perception::sanitize_untrusted(&parcel.name),
                description: crate::perception::sanitize_untrusted(&parcel.desc),
                area: Some(parcel.area),
                category: Some(format!("{:?}", parcel.category)),
            });
        if let Some(data) = self
            .client
            .environment()
            .region_environment()
            .and_then(|message| message.environment())
        {
            // Read the environment data from the manager's last region message.
            let _ = data.day_cycle();
        }
        let mut terrain_height = 0.0;
        let terrain_observed = agent_position.x.is_finite()
            && agent_position.y.is_finite()
            && simulator
                .terrain_height_at_point(
                    agent_position.x.floor() as i32,
                    agent_position.y.floor() as i32,
                    &mut terrain_height,
                )
                .unwrap_or(false);
        let environment = Some(crate::perception::EnvironmentSnapshot {
            water_height: Some(simulator.water_height),
            terrain_height: terrain_observed.then_some(terrain_height),
            sun_phase: None,
            region_access: Some(format!("{:?}", simulator.access)),
        });
        let (inventory, landmarks, inventory_truncated) = native_inventory_metadata(&self.client);
        Ok(crate::perception::WorldSnapshot {
            generation,
            observed_unix_millis: world.value.observed_unix_millis,
            region_id: simulator.region_id,
            region_name: crate::perception::sanitize_untrusted(&simulator.name),
            agent: crate::perception::AgentSnapshot {
                id: self.agent.agent_id(),
                position: Some(position),
                health: Some(self.agent.health()),
                sitting_on_local_id: (self.agent.sitting_on() != 0)
                    .then_some(self.agent.sitting_on()),
            },
            avatars,
            avatars_truncated,
            objects,
            objects_truncated,
            parcel,
            environment,
            inventory,
            inventory_truncated,
            landmarks,
        })
    }
}

#[cfg(feature = "live-grid")]
impl crate::perception::WorldSnapshotSource for LibremetaverseWorldSnapshotSource {
    fn capture(
        &self,
        generation: u64,
        cancellation: CancellationToken,
    ) -> crate::perception::SnapshotFuture<'_> {
        let source = self.clone();
        Box::pin(async move {
            if cancellation.is_cancellation_requested() {
                return Err(crate::perception::PerceptionError::Cancelled);
            }
            let worker = tokio::task::spawn_blocking(move || source.capture_blocking(generation));
            tokio::select! {
                () = cancellation.cancelled() => Err(crate::perception::PerceptionError::Cancelled),
                result = worker => result
                    .map_err(|_| crate::perception::PerceptionError::SourceUnavailable)?,
            }
        })
    }
}

#[cfg(feature = "live-grid")]
impl LibremetaverseEmbodimentSink {
    fn ensure_generation(&self, generation: u64) -> Result<(), crate::behavior::BehaviorError> {
        let current = *self
            .generation
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        if current == Some(generation) && self.client.network().native_connected() {
            Ok(())
        } else {
            Err(crate::behavior::BehaviorError::NotReady)
        }
    }

    fn native_pose(
        &self,
        generation: u64,
    ) -> Result<crate::behavior::EmbodiedPose, crate::behavior::BehaviorError> {
        self.ensure_generation(generation)?;
        let simulator = self
            .client
            .network()
            .native_current_sim()
            .ok_or(crate::behavior::BehaviorError::NotReady)?;
        let position = self.agent.sim_position();
        let rotation = self.agent.sim_rotation();
        let sin_yaw = 2.0 * f64::from(rotation.w * rotation.z + rotation.x * rotation.y);
        let cos_yaw = 1.0 - 2.0 * f64::from(rotation.y * rotation.y + rotation.z * rotation.z);
        Ok(crate::behavior::EmbodiedPose {
            generation,
            region_id: simulator.region_id,
            position: native_position(position),
            heading_degrees: sin_yaw.atan2(cos_yaw).to_degrees().rem_euclid(360.0),
            sitting: self.agent.sitting_on() != 0,
        })
    }
}

#[cfg(feature = "live-grid")]
#[allow(clippy::cast_possible_truncation)]
impl crate::behavior::EmbodimentSink for LibremetaverseEmbodimentSink {
    fn current_pose(
        &self,
        generation: u64,
        cancellation: CancellationToken,
    ) -> crate::behavior::EmbodimentFuture<'_, crate::behavior::EmbodiedPose> {
        Box::pin(async move {
            if cancellation.is_cancellation_requested() {
                return Err(crate::behavior::BehaviorError::Cancelled);
            }
            self.native_pose(generation)
        })
    }

    fn resolve_avatar(
        &self,
        generation: u64,
        avatar_id: UUID,
        cancellation: CancellationToken,
    ) -> crate::behavior::EmbodimentFuture<'_, crate::perception::WorldPosition> {
        Box::pin(async move {
            if cancellation.is_cancellation_requested() {
                return Err(crate::behavior::BehaviorError::Cancelled);
            }
            self.ensure_generation(generation)?;
            let simulator = self
                .client
                .network()
                .native_current_sim()
                .ok_or(crate::behavior::BehaviorError::NotReady)?;
            let object_position = simulator
                .objects_avatars
                .read()
                .unwrap_or_else(std::sync::PoisonError::into_inner)
                .values()
                .find(|avatar| avatar.id == avatar_id)
                .map(|avatar| native_position(avatar.position));
            let coarse_position = self
                .client
                .grid()
                .native_coarse_position(simulator.handle, avatar_id);
            object_position
                .or_else(|| {
                    let origin = self.agent.sim_position();
                    coarse_position.map(|position| crate::perception::WorldPosition {
                        x: unwrap_coarse_axis(position.x, origin.x),
                        y: unwrap_coarse_axis(position.y, origin.y),
                        z: f64::from(position.z),
                    })
                })
                .ok_or(crate::behavior::BehaviorError::TargetUnavailable)
        })
    }

    fn face_point(
        &self,
        generation: u64,
        point: crate::perception::WorldPosition,
        cancellation: CancellationToken,
    ) -> crate::behavior::EmbodimentFuture<'_, ()> {
        Box::pin(async move {
            if cancellation.is_cancellation_requested() {
                return Err(crate::behavior::BehaviorError::Cancelled);
            }
            self.ensure_generation(generation)?;
            self.agent
                .movement
                .turn_toward(
                    libremetaverse_types::Vector3 {
                        x: point.x as f32,
                        y: point.y as f32,
                        z: point.z as f32,
                    },
                    Some(true),
                )
                .map_err(|_| crate::behavior::BehaviorError::NativeOperation)?;
            Ok(())
        })
    }

    fn begin_walk(
        &self,
        generation: u64,
        point: crate::perception::WorldPosition,
        cancellation: CancellationToken,
    ) -> crate::behavior::EmbodimentFuture<'_, ()> {
        Box::pin(async move {
            if cancellation.is_cancellation_requested() {
                return Err(crate::behavior::BehaviorError::Cancelled);
            }
            self.ensure_generation(generation)?;
            self.agent
                .auto_pilot_local(
                    point.x.round() as i32,
                    point.y.round() as i32,
                    point.z as f32,
                )
                .map_err(|_| crate::behavior::BehaviorError::NativeOperation)
        })
    }

    fn validate_walk_target(
        &self,
        generation: u64,
        point: crate::perception::WorldPosition,
        cancellation: CancellationToken,
    ) -> crate::behavior::EmbodimentFuture<'_, ()> {
        Box::pin(async move {
            if cancellation.is_cancellation_requested() {
                return Err(crate::behavior::BehaviorError::Cancelled);
            }
            self.ensure_generation(generation)?;
            let simulator = self
                .client
                .network()
                .native_current_sim()
                .ok_or(crate::behavior::BehaviorError::NotReady)?;
            let (region_size_x, region_size_y) = simulator.region_size();
            let current_position = self.agent.sim_position();
            let reported_bounds_contain_agent = f64::from(current_position.x)
                <= f64::from(region_size_x)
                && f64::from(current_position.y) <= f64::from(region_size_y);
            if point.x < 0.0
                || point.y < 0.0
                || (reported_bounds_contain_agent
                    && (point.x > f64::from(region_size_x) || point.y > f64::from(region_size_y)))
            {
                return Err(crate::behavior::BehaviorError::RegionBoundary);
            }
            let parcels = simulator
                .parcels
                .read()
                .unwrap_or_else(std::sync::PoisonError::into_inner);
            let parcel_at = |x: f32, y: f32| {
                parcels
                    .values()
                    .filter(|parcel| {
                        x >= parcel.aabb_min.x
                            && x <= parcel.aabb_max.x
                            && y >= parcel.aabb_min.y
                            && y <= parcel.aabb_max.y
                    })
                    .map(|parcel| parcel.local_id)
                    .min()
            };
            let current = parcel_at(current_position.x, current_position.y);
            let target = parcel_at(point.x as f32, point.y as f32);
            if current
                .zip(target)
                .is_some_and(|(current, target)| current != target)
                || (reported_bounds_contain_agent && (current.is_none() || target.is_none()))
            {
                return Err(crate::behavior::BehaviorError::RegionBoundary);
            }
            Ok(())
        })
    }

    fn stop(
        &self,
        generation: u64,
        cancellation: CancellationToken,
    ) -> crate::behavior::EmbodimentFuture<'_, ()> {
        Box::pin(async move {
            if cancellation.is_cancellation_requested() {
                return Err(crate::behavior::BehaviorError::Cancelled);
            }
            self.ensure_generation(generation)?;
            self.agent
                .auto_pilot_cancel()
                .map(|_| ())
                .map_err(|_| crate::behavior::BehaviorError::NativeOperation)
        })
    }

    fn sit(
        &self,
        generation: u64,
        cancellation: CancellationToken,
    ) -> crate::behavior::EmbodimentFuture<'_, ()> {
        Box::pin(async move {
            if cancellation.is_cancellation_requested() {
                return Err(crate::behavior::BehaviorError::Cancelled);
            }
            self.ensure_generation(generation)?;
            self.agent
                .sit()
                .map_err(|_| crate::behavior::BehaviorError::NativeOperation)
        })
    }

    fn stand(
        &self,
        generation: u64,
        cancellation: CancellationToken,
    ) -> crate::behavior::EmbodimentFuture<'_, ()> {
        Box::pin(async move {
            if cancellation.is_cancellation_requested() {
                return Err(crate::behavior::BehaviorError::Cancelled);
            }
            self.ensure_generation(generation)?;
            self.agent
                .stand()
                .map(|_| ())
                .map_err(|_| crate::behavior::BehaviorError::NativeOperation)
        })
    }

    fn set_camera(
        &self,
        generation: u64,
        position: crate::perception::WorldPosition,
        target: crate::perception::WorldPosition,
        vertical_fov_degrees: f64,
        cancellation: CancellationToken,
    ) -> crate::behavior::EmbodimentFuture<'_, ()> {
        Box::pin(async move {
            if cancellation.is_cancellation_requested() {
                return Err(crate::behavior::BehaviorError::Cancelled);
            }
            self.ensure_generation(generation)?;
            self.agent
                .movement
                .camera
                .look_at_with_vector3_vector3(
                    libremetaverse_types::Vector3 {
                        x: position.x as f32,
                        y: position.y as f32,
                        z: position.z as f32,
                    },
                    libremetaverse_types::Vector3 {
                        x: target.x as f32,
                        y: target.y as f32,
                        z: target.z as f32,
                    },
                )
                .map_err(|_| crate::behavior::BehaviorError::NativeOperation)?;
            self.agent
                .movement
                .set_fov_vertical_angle((vertical_fov_degrees as f32).to_radians())
                .and_then(|()| self.agent.movement.send_update_with_boolean(Some(true)))
                .map_err(|_| crate::behavior::BehaviorError::NativeOperation)
        })
    }

    fn reset_camera(
        &self,
        generation: u64,
        cancellation: CancellationToken,
    ) -> crate::behavior::EmbodimentFuture<'_, ()> {
        Box::pin(async move {
            if cancellation.is_cancellation_requested() {
                return Err(crate::behavior::BehaviorError::Cancelled);
            }
            self.ensure_generation(generation)?;
            native_reset_camera(&self.agent)
                .and_then(|()| self.agent.movement.send_update_with_boolean(Some(true)))
                .map_err(|_| crate::behavior::BehaviorError::NativeOperation)
        })
    }
}

#[cfg(feature = "live-grid")]
fn unwrap_coarse_axis(encoded: f32, origin: f32) -> f64 {
    f64::from(encoded + ((origin - encoded) / 256.0).round() * 256.0)
}

#[cfg(all(test, feature = "live-grid"))]
mod live_grid_tests {
    #[test]
    fn coarse_avatar_axis_unwraps_to_nearest_varregion_tile() {
        assert!((super::unwrap_coarse_axis(164.0, 678.0) - 676.0).abs() < f64::EPSILON);
        assert!((super::unwrap_coarse_axis(26.0, 539.0) - 538.0).abs() < f64::EPSILON);
    }
}

#[cfg(feature = "live-grid")]
fn native_position(value: libremetaverse_types::Vector3) -> crate::perception::WorldPosition {
    crate::perception::WorldPosition {
        x: f64::from(value.x),
        y: f64::from(value.y),
        z: f64::from(value.z),
    }
}

#[cfg(feature = "live-grid")]
fn native_reset_camera(agent: &libremetaverse::AgentManager) -> Result<(), libremetaverse::Error> {
    let avatar = agent.sim_position();
    let forward = libremetaverse_types::Vector3::mul_with_vector3_quaternion(
        libremetaverse_types::Vector3::unit_x(),
        agent.sim_rotation(),
    );
    let position = libremetaverse_types::Vector3 {
        x: avatar.x,
        y: avatar.y,
        z: avatar.z + 1.6,
    };
    let target = libremetaverse_types::Vector3 {
        x: position.x + forward.x * 4.0,
        y: position.y + forward.y * 4.0,
        z: position.z + forward.z * 4.0,
    };
    agent
        .movement
        .camera
        .look_at_with_vector3_vector3(position, target)?;
    agent.movement.set_fov_vertical_angle(60.0_f32.to_radians())
}

#[cfg(feature = "live-grid")]
fn native_inventory_metadata(
    client: &libremetaverse::GridClient,
) -> (
    Vec<crate::perception::InventoryMetadata>,
    Vec<crate::perception::LandmarkMetadata>,
    bool,
) {
    let Some(store) = client.inventory().store() else {
        return (Vec::new(), Vec::new(), false);
    };
    let mut inventory = Vec::new();
    let mut landmarks = Vec::new();
    let mut pending = vec![(store.root_node(), String::new())];
    let mut visited = 0_usize;
    let mut truncated = false;
    while let Some((node, parent_path)) = pending.pop() {
        if visited >= 2_048 {
            truncated = true;
            break;
        }
        visited += 1;
        let path = node.data().map_or_else(
            || parent_path.clone(),
            |data| {
                let base = data.inventory_base();
                let name = crate::perception::sanitize_untrusted(&base.name());
                let path = crate::perception::sanitize_untrusted(&if parent_path.is_empty() {
                    format!("/{name}")
                } else {
                    format!("{parent_path}/{name}")
                });
                if let Some(item) = data.inventory_item() {
                    let modified_unix_millis = item
                        .creation_date()
                        .duration_since(std::time::UNIX_EPOCH)
                        .ok()
                        .and_then(|duration| u64::try_from(duration.as_millis()).ok());
                    inventory.push(crate::perception::InventoryMetadata {
                        id: base.uuid(),
                        name: name.clone(),
                        kind: format!("{:?}", item.inventory_type()),
                        path: path.clone(),
                        modified_unix_millis,
                    });
                    if item.asset_type() == libremetaverse_types::AssetType::Landmark {
                        landmarks.push(crate::perception::LandmarkMetadata {
                            id: base.uuid(),
                            name,
                            // Region is stored in the landmark asset body. The
                            // read-only metadata tool never downloads or decodes it.
                            region_id: None,
                        });
                    }
                }
                path
            },
        );
        let mut children = node.nodes().values();
        children.sort_by_key(|child| {
            child.data().map_or_else(String::new, |value| {
                crate::perception::sanitize_untrusted(&value.inventory_base().name())
            })
        });
        for child in children.into_iter().rev() {
            pending.push((child, path.clone()));
        }
    }
    (inventory, landmarks, truncated || !pending.is_empty())
}

#[cfg(feature = "live-grid")]
#[derive(Clone)]
pub struct LibremetaverseInteractionSink {
    agent: Arc<libremetaverse::AgentManager>,
    delivery_generation: Arc<std::sync::RwLock<Option<u64>>>,
}

#[cfg(feature = "live-grid")]
impl fmt::Debug for LibremetaverseInteractionSink {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("LibremetaverseInteractionSink")
            .field("agent", &"[NATIVE AGENT MANAGER]")
            .finish_non_exhaustive()
    }
}

#[cfg(feature = "live-grid")]
impl crate::interaction::InteractionSink for LibremetaverseInteractionSink {
    fn deliver(
        &self,
        outbound: crate::interaction::OutboundInteraction,
        cancellation: CancellationToken,
    ) -> crate::interaction::DeliveryFuture<'_> {
        let result = if cancellation.is_cancellation_requested() {
            Err(crate::interaction::InteractionDeliveryError::Disconnected)
        } else {
            let generation = self
                .delivery_generation
                .read()
                .unwrap_or_else(std::sync::PoisonError::into_inner);
            if *generation == Some(outbound.generation) {
                match outbound.channel {
                    crate::interaction::InteractionChannel::PublicChat => self.agent.chat(
                        outbound.body.into_inner(),
                        0,
                        libremetaverse::ChatType::Normal,
                        Some(false),
                    ),
                    crate::interaction::InteractionChannel::DirectIm => {
                        self.agent.instant_message_with_uuid_string(
                            outbound.recipient_id,
                            outbound.body.into_inner(),
                        )
                    }
                }
                .map_err(|_| crate::interaction::InteractionDeliveryError::Failed)
            } else {
                Err(crate::interaction::InteractionDeliveryError::Disconnected)
            }
        };
        Box::pin(async move { result })
    }
}

#[cfg(feature = "live-grid")]
struct LibremetaverseSession {
    generation: u64,
    network: libremetaverse::NetworkManager,
    signals: mpsc::Receiver<crate::session::SessionSignal>,
    subscriptions: Vec<libremetaverse_types::compat::Subscription>,
    interaction: Option<crate::interaction::InteractionIngress>,
    perception: Option<crate::perception::PerceptionIngress>,
    behavior: Option<crate::behavior::BehaviorIngress>,
    agent: Arc<libremetaverse::AgentManager>,
    framework_readiness: Option<Arc<crate::session::FrameworkReadiness>>,
    delivery_generation: Arc<std::sync::RwLock<Option<u64>>>,
}

#[cfg(feature = "live-grid")]
impl crate::session::GridSession for LibremetaverseSession {
    fn generation(&self) -> u64 {
        self.generation
    }

    fn next_signal(
        &mut self,
        cancellation: CancellationToken,
    ) -> crate::session::SessionFuture<'_, crate::session::SessionSignal> {
        Box::pin(async move {
            let signal = tokio::select! {
                signal = self.signals.recv() => signal.unwrap_or_else(|| {
                    crate::session::SessionSignal::Disconnected(
                        crate::session::SessionFailure::new(
                            crate::session::SessionFailureKind::TransientTransport,
                        ),
                    )
                }),
                () = cancellation.cancelled() => crate::session::SessionSignal::Disconnected(
                    crate::session::SessionFailure::new(
                        crate::session::SessionFailureKind::TransientTransport,
                    ),
                ),
            };
            if matches!(signal, crate::session::SessionSignal::Ready) {
                if let Some(readiness) = &self.framework_readiness
                    && !readiness.wait_ready(self.generation, cancellation).await
                {
                    return crate::session::SessionSignal::Disconnected(
                        crate::session::SessionFailure::new(
                            crate::session::SessionFailureKind::TransientTransport,
                        ),
                    );
                }
                *self
                    .delivery_generation
                    .write()
                    .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(self.generation);
                if let Some(interaction) = &self.interaction {
                    let _ = interaction.try_connected(self.generation, self.agent.agent_id());
                }
                if let (Some(perception), Some(simulator)) =
                    (&self.perception, self.network.native_current_sim())
                {
                    let _ = perception.connected(self.generation, simulator.region_id);
                }
                if let (Some(behavior), Some(simulator)) =
                    (&self.behavior, self.network.native_current_sim())
                {
                    let _ = behavior.connected(self.generation, simulator.region_id);
                }
            }
            signal
        })
    }

    fn logout(
        mut self: Box<Self>,
        cancellation: CancellationToken,
    ) -> crate::session::SessionFuture<'static, Result<(), crate::session::SessionFailure>> {
        Box::pin(async move {
            if let Some(interaction) = &self.interaction {
                let _ = interaction.try_disconnected();
            }
            if let Some(perception) = &self.perception {
                perception.disconnected();
            }
            if let Some(behavior) = &self.behavior {
                behavior.disconnected();
                tokio::task::yield_now().await;
            }
            *self
                .delivery_generation
                .write()
                .unwrap_or_else(std::sync::PoisonError::into_inner) = None;
            self.subscriptions.clear();
            self.network
                .native_logout_async(Some(cancellation))
                .await
                .map_err(|_| {
                    crate::session::SessionFailure::new(
                        crate::session::SessionFailureKind::TransientTransport,
                    )
                })
        })
    }
}

#[cfg(feature = "live-grid")]
impl crate::session::GridSessionBackend for LibremetaverseSessionBackend {
    #[allow(clippy::too_many_lines)]
    fn login(
        &self,
        generation: u64,
        cancellation: CancellationToken,
    ) -> crate::session::SessionFuture<
        '_,
        Result<Box<dyn crate::session::GridSession>, crate::session::SessionFailure>,
    > {
        Box::pin(async move {
            *self
                .delivery_generation
                .write()
                .unwrap_or_else(std::sync::PoisonError::into_inner) = None;
            let mut start = "last";
            loop {
                let mut params = self
                    .network
                    .native_default_login_params(
                        self.first_name.clone(),
                        self.last_name.clone(),
                        self.password.expose_secret().to_owned(),
                        "MetaCrate".to_owned(),
                        env!("CARGO_PKG_VERSION").to_owned(),
                    )
                    .map_err(|_| {
                        crate::session::SessionFailure::new(
                            crate::session::SessionFailureKind::InvalidConfiguration,
                        )
                    })?;
                params.uri.clone_from(&self.login_url);
                start.clone_into(&mut params.start);
                let logged_in = self
                    .network
                    .native_login(params, Some(cancellation.clone()))
                    .await
                    .map_err(|_| {
                        crate::session::SessionFailure::new(
                            crate::session::SessionFailureKind::TransientTransport,
                        )
                    })?;
                if logged_in {
                    let simulator = self.network.current_sim().ok_or_else(|| {
                        crate::session::SessionFailure::new(
                            crate::session::SessionFailureKind::TransientTransport,
                        )
                    })?;
                    self.throttle
                        .set_with_simulator(Some(simulator.clone()))
                        .map_err(|_| {
                            crate::session::SessionFailure::new(
                                crate::session::SessionFailureKind::TransientTransport,
                            )
                        })?;
                    let readiness_simulator = simulator.clone();
                    let _ = tokio::task::spawn_blocking(move || {
                        readiness_simulator.native_is_event_queue_running(Some(true))
                    })
                    .await;
                    let _ = self
                        .grid
                        .get_grid_region_with_string_grid_layer_type_cancellation_token(
                            simulator.name.clone(),
                            libremetaverse::GridLayerType::Objects,
                            Some(cancellation.clone()),
                        )
                        .await;
                    native_reset_camera(&self.agent).map_err(|_| {
                        crate::session::SessionFailure::new(
                            crate::session::SessionFailureKind::TransientTransport,
                        )
                    })?;
                    break;
                }
                let error_key = self.network.native_login_error_key();
                let message = self.network.native_login_message();
                if start == "last" && native_last_location_unavailable(&message) {
                    start = "home";
                    continue;
                }
                return Err(classify_native_login_failure(&error_key, &message));
            }

            // Native login has already installed the current simulator and
            // parsed inventory state. The existing GridClient managers retain
            // world/inventory and movement-worker ownership; this generation
            // adds only one disconnect and one readiness subscription.
            let (sender, signals) = mpsc::channel(8);
            let disconnected_sender = sender.clone();
            let disconnected_interaction = self.interaction.clone();
            let disconnected_perception = self.perception.clone();
            let disconnected_behavior = self.behavior.clone();
            let disconnected_generation = Arc::clone(&self.delivery_generation);
            let disconnected = self
                .network
                .native_subscribe_disconnected(Arc::new(move |event| {
                    *disconnected_generation
                        .write()
                        .unwrap_or_else(std::sync::PoisonError::into_inner) = None;
                    if let Some(interaction) = &disconnected_interaction {
                        let _ = interaction.try_disconnected();
                    }
                    if let Some(perception) = &disconnected_perception {
                        perception.disconnected();
                    }
                    if let Some(behavior) = &disconnected_behavior {
                        behavior.disconnected();
                    }
                    let kind = match event.reason() {
                        libremetaverse::NetworkManagerDisconnectType::NetworkTimeout
                        | libremetaverse::NetworkManagerDisconnectType::ClientInitiated => {
                            crate::session::SessionFailureKind::TransientTransport
                        }
                        libremetaverse::NetworkManagerDisconnectType::ServerInitiated => {
                            crate::session::SessionFailureKind::Kicked
                        }
                        libremetaverse::NetworkManagerDisconnectType::SimShutdown => {
                            crate::session::SessionFailureKind::Maintenance
                        }
                    };
                    let _ =
                        disconnected_sender.try_send(crate::session::SessionSignal::Disconnected(
                            crate::session::SessionFailure::new(kind),
                        ));
                }));
            let ready_sender = sender.clone();
            let ready_once = Arc::new(AtomicBool::new(false));
            let callback_ready = Arc::clone(&ready_once);
            let ready = self
                .network
                .native_subscribe_event_queue_running(Arc::new(move |_| {
                    if !callback_ready.swap(true, Ordering::AcqRel) {
                        let _ = ready_sender.try_send(crate::session::SessionSignal::Ready);
                    }
                }));
            let mut subscriptions = vec![disconnected, ready];
            if let Some(perception) = &self.perception {
                let crossing_perception = perception.clone();
                let crossing_behavior = self.behavior.clone();
                let crossing_network = self.network.clone();
                subscriptions.push(self.network.native_subscribe_sim_changed(Arc::new(
                    move |_| {
                        if let Some(simulator) = crossing_network.native_current_sim() {
                            let _ =
                                crossing_perception.region_changed(generation, simulator.region_id);
                            if let Some(behavior) = &crossing_behavior {
                                let _ = behavior.region_changed(generation, simulator.region_id);
                            }
                        }
                    },
                )));
            }
            if let Some(interaction) = &self.interaction {
                let chat_ingress = interaction.clone();
                let chat_agent = Arc::clone(&self.agent);
                subscriptions.push(self.agent.subscribe_chat_from_simulator(Arc::new(
                    move |event| {
                        if let Some(inbound) = native_public_interaction(&chat_agent, &event) {
                            let _ = chat_ingress.try_submit(inbound);
                        }
                    },
                )));
                let im_ingress = interaction.clone();
                let im_agent = Arc::clone(&self.agent);
                subscriptions.push(self.agent.subscribe_im(Arc::new(move |event| {
                    if let Some(inbound) = native_im_interaction(&im_agent, &event) {
                        let _ = im_ingress.try_submit(inbound);
                    }
                })));
            }
            if !self.network.native_connected() {
                let _ = sender.try_send(crate::session::SessionSignal::Disconnected(
                    crate::session::SessionFailure::new(
                        crate::session::SessionFailureKind::TransientTransport,
                    ),
                ));
            } else if self
                .network
                .native_current_sim()
                .and_then(|simulator| simulator.native_is_event_queue_running(None).ok())
                .unwrap_or(false)
                && !ready_once.swap(true, Ordering::AcqRel)
            {
                let _ = sender.try_send(crate::session::SessionSignal::Ready);
            }
            let session: Box<dyn crate::session::GridSession> = Box::new(LibremetaverseSession {
                generation,
                network: self.network.clone(),
                signals,
                subscriptions,
                interaction: self.interaction.clone(),
                perception: self.perception.clone(),
                behavior: self.behavior.clone(),
                agent: Arc::clone(&self.agent),
                framework_readiness: self.framework_readiness.clone(),
                delivery_generation: Arc::clone(&self.delivery_generation),
            });
            Ok(session)
        })
    }

    fn flush_audit(
        &self,
        _cancellation: CancellationToken,
    ) -> crate::session::SessionFuture<'_, Result<(), crate::session::SessionFailure>> {
        Box::pin(async { Ok(()) })
    }
}

#[cfg(feature = "live-grid")]
fn native_public_interaction(
    agent: &libremetaverse::AgentManager,
    event: &libremetaverse::ChatEventArgs,
) -> Option<crate::interaction::InboundInteraction> {
    if event.audible_level() == libremetaverse::ChatAudibleLevel::Not
        || !matches!(
            event.type_(),
            libremetaverse::ChatType::Normal
                | libremetaverse::ChatType::Whisper
                | libremetaverse::ChatType::Shout
        )
    {
        return None;
    }
    let source = match event.source_type() {
        libremetaverse::ChatSourceType::Agent => crate::interaction::InboundSource::Avatar,
        libremetaverse::ChatSourceType::Object => crate::interaction::InboundSource::Object,
        libremetaverse::ChatSourceType::System => crate::interaction::InboundSource::System,
    };
    let source_id = event.source_id();
    let owner_id = event.owner_id();
    let message = event.message();
    let name = event.from_name();
    let bucket = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or(Duration::ZERO)
        .as_secs()
        / 5;
    let delivery_id = native_delivery_id(
        "chat",
        &[
            &source_id.to_string(),
            &owner_id.to_string(),
            &bucket.to_string(),
            &message,
        ],
    );
    crate::interaction::InboundInteraction::new(
        delivery_id,
        source_id,
        name,
        crate::interaction::InteractionChannel::PublicChat,
        source,
        crate::interaction::ImDialogKind::OneToOneMessage,
        event.audible_level() == libremetaverse::ChatAudibleLevel::Fully,
        native_muted(agent, source_id, owner_id),
        message,
    )
    .ok()
}

#[cfg(feature = "live-grid")]
fn native_im_interaction(
    agent: &libremetaverse::AgentManager,
    event: &libremetaverse::InstantMessageEventArgs,
) -> Option<crate::interaction::InboundInteraction> {
    let im = event.im();
    let dialog = match im.dialog {
        libremetaverse::InstantMessageDialog::MessageFromAgent if !im.group_im => {
            crate::interaction::ImDialogKind::OneToOneMessage
        }
        libremetaverse::InstantMessageDialog::StartTyping
        | libremetaverse::InstantMessageDialog::StopTyping => {
            crate::interaction::ImDialogKind::Typing
        }
        libremetaverse::InstantMessageDialog::SessionSend => {
            crate::interaction::ImDialogKind::GroupOrConference
        }
        _ => crate::interaction::ImDialogKind::Unsupported,
    };
    let timestamp = im
        .timestamp
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or(Duration::ZERO)
        .as_millis()
        .to_string();
    let delivery_id = native_delivery_id(
        "im",
        &[
            &im.from_agent_id.to_string(),
            &im.im_session_id.to_string(),
            &timestamp,
            &im.message,
        ],
    );
    crate::interaction::InboundInteraction::new(
        delivery_id,
        im.from_agent_id,
        im.from_agent_name,
        crate::interaction::InteractionChannel::DirectIm,
        crate::interaction::InboundSource::Avatar,
        dialog,
        false,
        native_muted(agent, im.from_agent_id, UUID::zero()),
        im.message,
    )
    .ok()
}

#[cfg(feature = "live-grid")]
fn native_muted(agent: &libremetaverse::AgentManager, source: UUID, owner: UUID) -> bool {
    agent
        .mute_list
        .read()
        .unwrap_or_else(std::sync::PoisonError::into_inner)
        .values()
        .any(|entry| entry.id == source || (owner != UUID::zero() && entry.id == owner))
}

#[cfg(feature = "live-grid")]
fn native_delivery_id(prefix: &str, fields: &[&str]) -> String {
    use sha2::Digest as _;
    use std::fmt::Write as _;
    let mut digest = sha2::Sha256::new();
    for field in fields {
        digest.update(field.as_bytes());
        digest.update([0]);
    }
    let mut output = String::with_capacity(prefix.len() + 65);
    output.push_str(prefix);
    output.push('-');
    for byte in digest.finalize() {
        let _ = write!(output, "{byte:02x}");
    }
    output
}

#[cfg(feature = "live-grid")]
fn classify_native_login_failure(error_key: &str, message: &str) -> crate::session::SessionFailure {
    let normalized = error_key.trim().to_ascii_lowercase();
    let kind = if native_last_location_unavailable(message)
        || message
            .trim()
            .to_ascii_lowercase()
            .contains("already logged in")
    {
        crate::session::SessionFailureKind::ServerFailure
    } else if matches!(
        normalized.as_str(),
        "key" | "password" | "credential" | "account" | "username" | "user"
    ) {
        crate::session::SessionFailureKind::InvalidCredentials
    } else if normalized == "canceled" {
        crate::session::SessionFailureKind::TransientTransport
    } else {
        crate::session::SessionFailureKind::ServerFailure
    };
    crate::session::SessionFailure::new(kind)
}

#[cfg(feature = "live-grid")]
fn native_last_location_unavailable(message: &str) -> bool {
    let message = message.trim().to_ascii_lowercase();
    message.contains("failed to verify user presence")
        || message.contains("access denied to region")
}

impl GridBackend for OfflineGridBackend {
    fn name(&self) -> &'static str {
        "offline-fake"
    }

    fn run(
        &self,
        events: mpsc::Sender<GridEvent>,
        cancellation: CancellationToken,
    ) -> BackendFuture<'_, Result<(), BackendError>> {
        Box::pin(async move {
            let ready = GridEvent {
                sequence: 1,
                kind: GridEventKind::BackendReady,
            };
            tokio::select! {
                () = cancellation.cancelled() => return Ok(()),
                result = events.send(ready) => {
                    result.map_err(|_| BackendError::EventQueueClosed)?;
                }
            }
            cancellation.cancelled().await;
            Ok(())
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use libremetaverse_types::compat::CancellationTokenSource;

    #[tokio::test]
    async fn offline_backend_stops_on_cancellation() {
        let backend = OfflineGridBackend::new();
        let (sender, mut receiver) = mpsc::channel(1);
        let cancellation = CancellationTokenSource::new();
        let run = backend.run(sender, cancellation.token());
        tokio::pin!(run);
        tokio::select! {
            event = receiver.recv() => {
                assert_eq!(event.expect("ready event").kind, GridEventKind::BackendReady);
            }
            result = &mut run => panic!("backend exited before ready: {result:?}"),
        }
        cancellation.cancel();
        run.await.expect("clean cancellation");
    }

    #[cfg(feature = "live-grid")]
    #[test]
    fn stale_presence_is_retryable_without_masking_bad_credentials() {
        assert!(native_last_location_unavailable(
            "Failed to verify user presence in the grid, access denied to region",
        ));
        assert_eq!(
            classify_native_login_failure(
                "account",
                "Failed to verify user presence in the grid, access denied to region",
            )
            .kind(),
            crate::session::SessionFailureKind::ServerFailure,
        );
        assert_eq!(
            classify_native_login_failure("account", "Invalid credentials").kind(),
            crate::session::SessionFailureKind::InvalidCredentials,
        );
    }

    #[cfg(feature = "live-grid")]
    #[test]
    fn asset_cache_is_platform_scoped_to_metacrate() {
        let owner = LibremetaverseClientOwner::new().expect("client owner");
        let settings = owner.client().settings_ref().asset_cache();
        let cache = std::path::PathBuf::from(settings.dir);
        assert!(cache.is_absolute());
        assert!(
            cache
                .components()
                .any(|part| part.as_os_str() == "metacrate")
        );
        assert_eq!(settings.max_size, 2 * 1024 * 1024 * 1024);
    }
}
