//! Estate administration decisions, owner-message payloads, and capabilities.

#![allow(clippy::missing_errors_doc)]
#![allow(clippy::must_use_candidate)]
#![allow(clippy::needless_pass_by_value)]
#![allow(clippy::similar_names)]
#![allow(clippy::too_many_arguments)]
#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::fn_params_excessive_bools)]
#![allow(clippy::unnecessary_wraps)]

use crate::agent_manager::EventRegistry;
use crate::packet_catalog::{GeneratedPacket, PacketType};
use crate::{
    Error, EstateToolsEstateAccessDelta as AccessDelta, EstateToolsEstateReturnFlags,
    EstateToolsGroundTextureHeight, EstateToolsGroundTextureHeightSettings,
    EstateToolsGroundTextureSettings, EstateToolsLandStatReportType, EstateToolsRegionMaturity,
    GridClient, RegionFlags, RegionRestartDays, SourceType,
};
use futures_channel::oneshot;
use futures_util::{FutureExt, pin_mut, select_biased};
use libremetaverse_structured_data::{OSD, OSDParser};
use libremetaverse_types::AssetType;
use libremetaverse_types::compat::{CancellationToken, EventHandler, Subscription, Uri};
use libremetaverse_types::{UUID, Vector3};
use serde_json::Value;
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;

fn blank_textures() -> EstateToolsGroundTextureSettings {
    EstateToolsGroundTextureSettings {
        low: UUID::zero(),
        mid_low: UUID::zero(),
        mid_high: UUID::zero(),
        high: UUID::zero(),
    }
}

fn blank_limits() -> EstateToolsGroundTextureHeightSettings {
    let height = || EstateToolsGroundTextureHeight {
        low: 0.0,
        high: 0.0,
    };
    EstateToolsGroundTextureHeightSettings {
        sw: height(),
        nw: height(),
        se: height(),
        ne: height(),
    }
}

#[derive(Clone, Debug)]
pub struct RegionRestartSchedule {
    daily: bool,
    days: RegionRestartDays,
    time: Duration,
}
impl RegionRestartSchedule {
    pub fn new() -> Result<Self, Error> {
        Ok(Self {
            daily: false,
            days: RegionRestartDays::NONE,
            time: Duration::ZERO,
        })
    }
    pub const fn is_daily(&self) -> bool {
        self.daily
    }
    pub fn set_is_daily(&mut self, value: bool) {
        self.daily = value;
    }
    pub const fn days(&self) -> RegionRestartDays {
        self.days
    }
    pub fn set_days(&mut self, value: RegionRestartDays) {
        self.days = value;
    }
    pub const fn time(&self) -> Duration {
        self.time
    }
    pub fn set_time(&mut self, value: Duration) {
        self.time = value;
    }
}

#[derive(Clone, Debug, Default)]
pub struct EstateTask {
    pub position: Vector3,
    pub score: f32,
    pub mono_score: f32,
    pub task_id: UUID,
    pub task_local_id: u32,
    pub task_name: String,
    pub owner_name: String,
}
impl EstateTask {
    pub fn new() -> Result<Self, Error> {
        Ok(Self::default())
    }
}

macro_rules! list_args {
    ($name:ident, $field:ident, $getter:ident) => {
        #[derive(Clone, Debug)]
        pub struct $name {
            estate_id: u32,
            count: i32,
            $field: Vec<UUID>,
        }
        impl $name {
            pub fn new(estate_id: u32, count: i32, $field: Vec<UUID>) -> Result<Self, Error> {
                Ok(Self {
                    estate_id,
                    count,
                    $field,
                })
            }
            pub const fn estate_id(&self) -> u32 {
                self.estate_id
            }
            pub const fn count(&self) -> i32 {
                self.count
            }
            pub fn $getter(&self) -> Vec<UUID> {
                self.$field.clone()
            }
        }
    };
}
list_args!(EstateBansReplyEventArgs, banned, banned);
list_args!(EstateGroupsReplyEventArgs, allowed_groups, allowed_groups);
list_args!(EstateManagersReplyEventArgs, managers, managers);
list_args!(EstateUsersReplyEventArgs, allowed_users, allowed_users);

#[derive(Clone, Debug)]
pub struct EstateExperienceReplyEventArgs {
    blocked: Vec<UUID>,
    trusted: Vec<UUID>,
    allowed: Vec<UUID>,
}
impl EstateExperienceReplyEventArgs {
    pub fn new(blocked: Vec<UUID>, trusted: Vec<UUID>, allowed: Vec<UUID>) -> Result<Self, Error> {
        Ok(Self {
            blocked,
            trusted,
            allowed,
        })
    }
    pub fn blocked(&self) -> Vec<UUID> {
        self.blocked.clone()
    }
    pub fn trusted(&self) -> Vec<UUID> {
        self.trusted.clone()
    }
    pub fn allowed(&self) -> Vec<UUID> {
        self.allowed.clone()
    }
}

#[derive(Clone, Debug)]
pub struct EstateCovenantReplyEventArgs {
    covenant_id: UUID,
    timestamp: i64,
    estate_name: String,
    estate_owner_id: UUID,
}
impl EstateCovenantReplyEventArgs {
    pub fn new(
        covenant_id: UUID,
        timestamp: i64,
        estate_name: String,
        estate_owner_id: UUID,
    ) -> Result<Self, Error> {
        Ok(Self {
            covenant_id,
            timestamp,
            estate_name,
            estate_owner_id,
        })
    }
    pub const fn covenant_id(&self) -> UUID {
        self.covenant_id
    }
    pub const fn timestamp(&self) -> i64 {
        self.timestamp
    }
    pub fn estate_name(&self) -> String {
        self.estate_name.clone()
    }
    pub const fn estate_owner_id(&self) -> UUID {
        self.estate_owner_id
    }
}

#[derive(Clone, Debug)]
pub struct EstateUpdateInfoReplyEventArgs {
    estate_name: String,
    estate_owner: UUID,
    estate_id: u32,
    flags: RegionFlags,
    sun_hour: i32,
}
impl EstateUpdateInfoReplyEventArgs {
    pub fn new(
        estate_name: String,
        estate_owner: UUID,
        estate_id: u32,
        flags: RegionFlags,
        sun_hour: i32,
    ) -> Result<Self, Error> {
        Ok(Self {
            estate_name,
            estate_owner,
            estate_id,
            flags,
            sun_hour,
        })
    }
    pub fn estate_name(&self) -> String {
        self.estate_name.clone()
    }
    pub const fn estate_owner(&self) -> UUID {
        self.estate_owner
    }
    pub const fn estate_id(&self) -> u32 {
        self.estate_id
    }
    pub const fn flags(&self) -> RegionFlags {
        self.flags
    }
    pub const fn sun_hour(&self) -> i32 {
        self.sun_hour
    }
}

macro_rules! task_args {
    ($name:ident) => {
        #[derive(Clone, Debug)]
        pub struct $name {
            object_count: i32,
            tasks: HashMap<UUID, EstateTask>,
        }
        impl $name {
            pub fn new(object_count: i32, tasks: HashMap<UUID, EstateTask>) -> Result<Self, Error> {
                Ok(Self {
                    object_count,
                    tasks,
                })
            }
            pub const fn object_count(&self) -> i32 {
                self.object_count
            }
            pub fn tasks(&self) -> HashMap<UUID, EstateTask> {
                self.tasks.clone()
            }
        }
    };
}
task_args!(TopCollidersReplyEventArgs);
task_args!(TopScriptsReplyEventArgs);

#[derive(Default)]
struct EstateEvents {
    bans: EventRegistry<EstateBansReplyEventArgs>,
    covenant: EventRegistry<EstateCovenantReplyEventArgs>,
    experiences: EventRegistry<EstateExperienceReplyEventArgs>,
    groups: EventRegistry<EstateGroupsReplyEventArgs>,
    managers: EventRegistry<EstateManagersReplyEventArgs>,
    update: EventRegistry<EstateUpdateInfoReplyEventArgs>,
    users: EventRegistry<EstateUsersReplyEventArgs>,
    colliders: EventRegistry<TopCollidersReplyEventArgs>,
    scripts: EventRegistry<TopScriptsReplyEventArgs>,
}

pub(crate) struct EstateToolsInner {
    client: GridClient,
    network: crate::NetworkManager,
    events: EstateEvents,
    disposed: AtomicBool,
    live_mutations: AtomicBool,
    sim_console_lock: tokio::sync::Mutex<()>,
    subscriptions: Mutex<Vec<Subscription>>,
}

pub struct EstateTools {
    pub ground_texture_limits: EstateToolsGroundTextureHeightSettings,
    pub ground_textures: EstateToolsGroundTextureSettings,
    pub(crate) inner: Arc<EstateToolsInner>,
}

impl Clone for EstateTools {
    fn clone(&self) -> Self {
        Self {
            ground_texture_limits: EstateToolsGroundTextureHeightSettings {
                sw: EstateToolsGroundTextureHeight {
                    low: self.ground_texture_limits.sw.low,
                    high: self.ground_texture_limits.sw.high,
                },
                nw: EstateToolsGroundTextureHeight {
                    low: self.ground_texture_limits.nw.low,
                    high: self.ground_texture_limits.nw.high,
                },
                se: EstateToolsGroundTextureHeight {
                    low: self.ground_texture_limits.se.low,
                    high: self.ground_texture_limits.se.high,
                },
                ne: EstateToolsGroundTextureHeight {
                    low: self.ground_texture_limits.ne.low,
                    high: self.ground_texture_limits.ne.high,
                },
            },
            ground_textures: EstateToolsGroundTextureSettings {
                low: self.ground_textures.low,
                mid_low: self.ground_textures.mid_low,
                mid_high: self.ground_textures.mid_high,
                high: self.ground_textures.high,
            },
            inner: Arc::clone(&self.inner),
        }
    }
}

impl EstateTools {
    pub(crate) fn native_new(client: GridClient) -> Result<Self, Error> {
        let network = client.network();
        let candidate = Arc::new(EstateToolsInner {
            client: client.clone(),
            network: network.clone(),
            events: EstateEvents::default(),
            disposed: AtomicBool::new(false),
            live_mutations: AtomicBool::new(false),
            sim_console_lock: tokio::sync::Mutex::new(()),
            subscriptions: Mutex::new(Vec::new()),
        });
        let weak = Arc::downgrade(&candidate);
        candidate
            .subscriptions
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .push(network.subscribe_raw_packet(Arc::new(move |event| {
                if let Some(inner) = weak.upgrade() {
                    inner.handle_packet(event);
                }
            })));
        let weak = Arc::downgrade(&candidate);
        candidate
            .subscriptions
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .push(network.subscribe_caps(
                "LandStatReply".into(),
                crate::CapsEventQueueCallback::from_handler(move |_name, message, _simulator| {
                    let Some(inner) = weak.upgrade() else {
                        return;
                    };
                    let Ok(mut reply) = crate::messages::linden::LandStatReplyMessage::new() else {
                        return;
                    };
                    if message
                        .serialize()
                        .and_then(|map| reply.deserialize(map))
                        .is_ok()
                    {
                        inner.handle_land_stat_caps(reply);
                    }
                }),
            ));
        let inner = client.install_estate_tools_inner(candidate);
        Ok(Self::native_from_inner(inner))
    }
    pub(crate) fn native_from_inner(inner: Arc<EstateToolsInner>) -> Self {
        Self {
            ground_texture_limits: blank_limits(),
            ground_textures: blank_textures(),
            inner,
        }
    }
    pub(crate) fn native_inner(&self) -> Arc<EstateToolsInner> {
        Arc::clone(&self.inner)
    }
    pub fn new(client: GridClient) -> Result<Self, Error> {
        Self::native_new(client)
    }
    pub fn enable_live_mutations(&self) {
        self.inner.live_mutations.store(true, Ordering::Release);
    }
    pub fn dispose(&self) -> Result<(), Error> {
        self.inner.disposed.store(true, Ordering::Release);
        self.inner
            .subscriptions
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .clear();
        Ok(())
    }

    pub fn subscribe_estate_bans_reply(
        &self,
        h: EventHandler<EstateBansReplyEventArgs>,
    ) -> Subscription {
        self.inner.events.bans.subscribe(h)
    }
    pub fn subscribe_estate_covenant_reply(
        &self,
        h: EventHandler<EstateCovenantReplyEventArgs>,
    ) -> Subscription {
        self.inner.events.covenant.subscribe(h)
    }
    pub fn subscribe_estate_experience_reply(
        &self,
        h: EventHandler<EstateExperienceReplyEventArgs>,
    ) -> Subscription {
        self.inner.events.experiences.subscribe(h)
    }
    pub fn subscribe_estate_groups_reply(
        &self,
        h: EventHandler<EstateGroupsReplyEventArgs>,
    ) -> Subscription {
        self.inner.events.groups.subscribe(h)
    }
    pub fn subscribe_estate_managers_reply(
        &self,
        h: EventHandler<EstateManagersReplyEventArgs>,
    ) -> Subscription {
        self.inner.events.managers.subscribe(h)
    }
    pub fn subscribe_estate_update_info_reply(
        &self,
        h: EventHandler<EstateUpdateInfoReplyEventArgs>,
    ) -> Subscription {
        self.inner.events.update.subscribe(h)
    }
    pub fn subscribe_estate_users_reply(
        &self,
        h: EventHandler<EstateUsersReplyEventArgs>,
    ) -> Subscription {
        self.inner.events.users.subscribe(h)
    }
    pub fn subscribe_top_colliders_reply(
        &self,
        h: EventHandler<TopCollidersReplyEventArgs>,
    ) -> Subscription {
        self.inner.events.colliders.subscribe(h)
    }
    pub fn subscribe_top_scripts_reply(
        &self,
        h: EventHandler<TopScriptsReplyEventArgs>,
    ) -> Subscription {
        self.inner.events.scripts.subscribe(h)
    }

    #[allow(dead_code)]
    pub(crate) fn estate_owner_message_handler(
        &self,
        packet: crate::packets::EstateOwnerMessagePacket,
        _simulator: crate::Simulator,
    ) -> Result<(), Error> {
        if self.inner.disposed.load(Ordering::Acquire) {
            return Err(Error::InvalidOperation);
        }
        self.inner.handle_owner_message(packet);
        Ok(())
    }

    fn ids(&self) -> (UUID, UUID, String) {
        let mut client = self.inner.client.clone();
        let agent = client.self_();
        (agent.agent_id(), agent.session_id(), agent.name())
    }

    fn current_sim(&self) -> Result<crate::Simulator, Error> {
        self.inner
            .client
            .native_network()?
            .native_current_sim()
            .ok_or(Error::InvalidOperation)
    }

    fn ensure_udp_mutation_allowed(&self) -> Result<(), Error> {
        let sim = self.current_sim()?;
        if sim.connected() && !self.inner.live_mutations.load(Ordering::Acquire) {
            return Err(Error::InvalidOperation);
        }
        Ok(())
    }

    fn send_packet<T: GeneratedPacket>(&self, packet: &T, kind: PacketType) -> Result<(), Error> {
        if self.inner.disposed.load(Ordering::Acquire) {
            return Err(Error::InvalidOperation);
        }
        let sim = self.current_sim()?;
        let bytes = packet.encode_packet()?;
        let result = sim.native_send_packet_data(
            bytes.clone(),
            i32::try_from(bytes.len()).map_err(|_| Error::Argument)?,
            kind,
            false,
        );
        match result {
            Err(Error::InvalidOperation) if !sim.connected() => Ok(()),
            other => other,
        }
    }

    pub fn estate_owner_message_with_string_list(
        &self,
        method: String,
        params: Vec<String>,
    ) -> Result<(), Error> {
        self.ensure_udp_mutation_allowed()?;
        if method.len() > 255 || params.len() > 255 || params.iter().any(|value| value.len() > 255)
        {
            return Err(Error::Argument);
        }
        let (agent, session, _) = self.ids();
        let mut packet = crate::packets::EstateOwnerMessagePacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent;
        packet.agent_data.session_id = session;
        packet.agent_data.transaction_id = UUID::zero();
        packet.method_data.invoice = UUID::random()?;
        packet.method_data.method = method.into_bytes();
        for value in params {
            let mut block =
                crate::packets::EstateOwnerMessagePacketParamListBlock::new_with_constructor()?;
            block.parameter = value.into_bytes();
            packet.param_list.push(block);
        }
        self.send_packet(&packet, PacketType::EstateOwnerMessage)
    }
    pub fn estate_owner_message_with_string_string(
        &self,
        method: String,
        param: String,
    ) -> Result<(), Error> {
        self.estate_owner_message_with_string_list(method, vec![param])
    }
    fn owner_message(&self, method: &str, params: Vec<String>) -> Result<(), Error> {
        self.estate_owner_message_with_string_list(method.into(), params)
    }
    fn access_delta(
        &self,
        target: UUID,
        local: AccessDelta,
        all: AccessDelta,
        all_estates: bool,
    ) -> Result<(), Error> {
        let (agent, _, _) = self.ids();
        self.owner_message(
            "estateaccessdelta",
            vec![
                agent.to_string(),
                (if all_estates {
                    all as u32
                } else {
                    local as u32
                })
                .to_string(),
                target.to_string(),
            ],
        )
    }
    pub fn ban_user(&self, id: UUID, all: bool) -> Result<(), Error> {
        self.access_delta(
            id,
            AccessDelta::BanUser,
            AccessDelta::BanUserAllEstates,
            all,
        )
    }
    pub fn unban_user(&self, id: UUID, all: bool) -> Result<(), Error> {
        self.access_delta(
            id,
            AccessDelta::UnbanUser,
            AccessDelta::UnbanUserAllEstates,
            all,
        )
    }
    pub fn add_estate_manager(&self, id: UUID, all: bool) -> Result<(), Error> {
        self.access_delta(
            id,
            AccessDelta::AddManager,
            AccessDelta::AddManagerAllEstates,
            all,
        )
    }
    pub fn remove_estate_manager(&self, id: UUID, all: bool) -> Result<(), Error> {
        self.access_delta(
            id,
            AccessDelta::RemoveManager,
            AccessDelta::RemoveManagerAllEstates,
            all,
        )
    }
    pub fn add_allowed_user(&self, id: UUID, all: bool) -> Result<(), Error> {
        self.access_delta(
            id,
            AccessDelta::AddUserAsAllowed,
            AccessDelta::AddAllowedAllEstates,
            all,
        )
    }
    pub fn remove_allowed_user(&self, id: UUID, all: bool) -> Result<(), Error> {
        self.access_delta(
            id,
            AccessDelta::RemoveUserAsAllowed,
            AccessDelta::RemoveUserAllowedAllEstates,
            all,
        )
    }
    pub fn add_allowed_group(&self, id: UUID, all: bool) -> Result<(), Error> {
        self.access_delta(
            id,
            AccessDelta::AddGroupAsAllowed,
            AccessDelta::AddGroupAllowedAllEstates,
            all,
        )
    }
    pub fn remove_allowed_group(&self, id: UUID, all: bool) -> Result<(), Error> {
        self.access_delta(
            id,
            AccessDelta::RemoveGroupAsAllowed,
            AccessDelta::RemoveGroupAllowedAllEstates,
            all,
        )
    }
    pub fn request_info(&self) -> Result<(), Error> {
        self.owner_message("getinfo", vec![String::new()])
    }
    pub fn kick_user(&self, id: UUID) -> Result<(), Error> {
        self.owner_message("kickestate", vec![id.to_string()])
    }
    pub fn restart_region_with_method(&self) -> Result<(), Error> {
        self.restart_region_with_int32(120)
    }
    pub fn restart_region_with_int32(&self, delay: i32) -> Result<(), Error> {
        self.owner_message("restart", vec![delay.clamp(30, 240).to_string()])
    }
    pub fn cancel_restart(&self) -> Result<(), Error> {
        self.owner_message("restart", vec!["-1".into()])
    }
    pub fn estate_message(&self, message: String) -> Result<(), Error> {
        let (_, _, name) = self.ids();
        self.owner_message("instantmessage", vec![name, message])
    }
    pub fn simulator_message(&self, message: String) -> Result<(), Error> {
        let (agent, _, name) = self.ids();
        self.owner_message(
            "simulatormessage",
            vec!["-1".into(), "-1".into(), agent.to_string(), name, message],
        )
    }
    pub fn teleport_home_user(&self, id: UUID) -> Result<(), Error> {
        let (agent, _, _) = self.ids();
        self.owner_message("teleporthomeuser", vec![agent.to_string(), id.to_string()])
    }
    pub fn teleport_home_all_users(&self) -> Result<(), Error> {
        let (agent, _, _) = self.ids();
        self.owner_message("teleporthomeallusers", vec![agent.to_string()])
    }
    pub fn request_top_scripts(&self) -> Result<(), Error> {
        self.land_stat_request(
            0,
            EstateToolsLandStatReportType::TopScripts,
            0,
            String::new(),
        )
    }
    pub fn request_top_colliders(&self) -> Result<(), Error> {
        self.land_stat_request(
            0,
            EstateToolsLandStatReportType::TopColliders,
            0,
            String::new(),
        )
    }

    pub fn set_region_debug(
        &self,
        scripts: bool,
        collisions: bool,
        physics: bool,
    ) -> Result<(), Error> {
        self.owner_message(
            "setregiondebug",
            vec![yn(scripts), yn(collisions), yn(physics)],
        )
    }
    pub fn set_region_info_udp(
        &self,
        terraform: bool,
        fly: bool,
        damage: bool,
        resell: bool,
        pushing: bool,
        join: bool,
        agents: f32,
        bonus: f32,
        maturity: EstateToolsRegionMaturity,
    ) -> Result<(), Error> {
        self.owner_message(
            "setregioninfo",
            vec![
                yn(terraform),
                yn(fly),
                yn(damage),
                yn(resell),
                agents.to_string(),
                bonus.to_string(),
                (maturity as i32).to_string(),
                yn(pushing),
                yn(join),
            ],
        )
    }
    pub fn set_terrain_variables(
        &self,
        water: f32,
        raise: f32,
        lower: f32,
        estate_sun: bool,
        fixed_sun: bool,
        sun: f32,
    ) -> Result<(), Error> {
        self.owner_message(
            "setregionterrain",
            vec![
                water.to_string(),
                raise.to_string(),
                lower.to_string(),
                yn(estate_sun),
                yn(fixed_sun),
                sun.to_string(),
                "Y".into(),
                "N".into(),
                "0.00".into(),
            ],
        )
    }
    pub fn set_region_terrain(
        &self,
        low: UUID,
        mid_low: UUID,
        mid_high: UUID,
        high: UUID,
    ) -> Result<(), Error> {
        self.owner_message(
            "texturedetail",
            vec![
                format!("0 {low}"),
                format!("1 {mid_low}"),
                format!("2 {mid_high}"),
                format!("3 {high}"),
            ],
        )?;
        self.owner_message("texturecommit", vec![String::new()])
    }
    pub fn set_region_terrain_heights(
        &self,
        low_sw: f32,
        high_sw: f32,
        low_nw: f32,
        high_nw: f32,
        low_se: f32,
        high_se: f32,
        low_ne: f32,
        high_ne: f32,
    ) -> Result<(), Error> {
        self.owner_message(
            "textureheights",
            vec![
                format!("0 {low_sw} {high_sw}"),
                format!("1 {low_nw} {high_nw}"),
                format!("2 {low_se} {high_se}"),
                format!("3 {low_ne} {high_ne}"),
            ],
        )?;
        self.owner_message("texturecommit", vec![String::new()])
    }

    pub fn land_stat_request(
        &self,
        parcel: i32,
        report: EstateToolsLandStatReportType,
        flags: u32,
        filter: String,
    ) -> Result<(), Error> {
        let (agent, session, _) = self.ids();
        let mut packet = crate::packets::LandStatRequestPacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent;
        packet.agent_data.session_id = session;
        packet.request_data.filter = filter.into_bytes();
        packet.request_data.parcel_local_id = parcel;
        packet.request_data.report_type = report as u32;
        packet.request_data.request_flags = flags;
        self.send_packet(&packet, PacketType::LandStatRequest)
    }
    pub fn request_covenant(&self) -> Result<(), Error> {
        let (agent, session, _) = self.ids();
        let mut packet = crate::packets::EstateCovenantRequestPacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent;
        packet.agent_data.session_id = session;
        self.send_packet(&packet, PacketType::EstateCovenantRequest)
    }
    pub fn sim_wide_return(
        &self,
        target: UUID,
        flag: EstateToolsEstateReturnFlags,
        estate_wide: bool,
    ) -> Result<(), Error> {
        if estate_wide {
            return self.owner_message(
                "estateobjectreturn",
                vec![flag.0.to_string(), target.to_string()],
            );
        }
        self.ensure_udp_mutation_allowed()?;
        let (agent, session, _) = self.ids();
        let mut packet = crate::packets::SimWideDeletesPacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent;
        packet.agent_data.session_id = session;
        packet.data_block.target_id = target;
        packet.data_block.flags = flag.0;
        self.send_packet(&packet, PacketType::SimWideDeletes)
    }

    async fn capability(
        &self,
        name: &str,
        token: &CancellationToken,
    ) -> Result<Option<Uri>, Error> {
        token.throw_if_cancellation_requested()?;
        let sim = self.current_sim()?;
        if let Some(caps) = sim.native_caps()
            && let Some(uri) = caps.capability_uri(name.into())?
        {
            return Ok(Some(uri));
        }
        let Some(seed) = sim.seed_capability() else {
            return Ok(None);
        };
        let client = sim.native_data_arc().client.clone();
        let (response, body) = client
            .native_http_caps_client()
            .get(seed, token.clone(), None)
            .await?;
        if !(200..300).contains(&response.status_code) {
            return Ok(None);
        }
        let OSD::Map(map) = OSDParser::deserialize_with_bytes(body)? else {
            return Ok(None);
        };
        Ok(map
            .get(name)
            .and_then(|value| value.as_uri().ok().flatten()))
    }

    pub async fn get_region_restart_schedule(
        &self,
        ct: Option<CancellationToken>,
    ) -> Result<Option<RegionRestartSchedule>, Error> {
        let token = ct.unwrap_or_default();
        let Some(uri) = self.capability("RegionSchedule", &token).await? else {
            return Ok(None);
        };
        let (response, body) = self
            .inner
            .client
            .native_http_caps_client()
            .get(uri, token, None)
            .await?;
        if !(200..300).contains(&response.status_code) {
            return Ok(None);
        }
        let root: Value = match serde_json::from_slice(&body) {
            Ok(value) => value,
            Err(_) => return Ok(None),
        };
        let Some(restart) = root.get("restart") else {
            return Ok(None);
        };
        let daily = restart.get("type").and_then(Value::as_str) != Some("W");
        let days = if daily {
            RegionRestartDays::ALL
        } else {
            letters_to_days(
                restart
                    .get("days")
                    .and_then(Value::as_str)
                    .unwrap_or_default(),
            )
        };
        let seconds = restart
            .get("time")
            .and_then(Value::as_u64)
            .unwrap_or_default();
        Ok(Some(RegionRestartSchedule {
            daily,
            days,
            time: Duration::from_secs(seconds),
        }))
    }

    pub async fn set_region_restart_schedule(
        &self,
        schedule: RegionRestartSchedule,
        ct: Option<CancellationToken>,
    ) -> Result<bool, Error> {
        let token = ct.unwrap_or_default();
        let Some(uri) = self.capability("RegionSchedule", &token).await? else {
            return Ok(false);
        };
        if !crate::is_offline_fixture_uri(&uri)
            && !self.inner.live_mutations.load(Ordering::Acquire)
        {
            return Err(Error::InvalidOperation);
        }
        let mut restart = HashMap::from([
            (
                "type".into(),
                OSD::String(if schedule.daily { "D" } else { "W" }.into()),
            ),
            (
                "time".into(),
                OSD::Integer(i32::try_from(schedule.time.as_secs()).unwrap_or(i32::MAX)),
            ),
        ]);
        if !schedule.daily {
            restart.insert("days".into(), OSD::String(days_to_letters(schedule.days)));
        }
        let body = OSDParser::serialize_llsd_xml_bytes(OSD::Map(HashMap::from([(
            "restart".into(),
            OSD::Map(restart),
        )])))?;
        let (response, _) = self
            .inner
            .client
            .native_http_caps_client()
            .post_with_uri_string_bytes_cancellation_token_i_progress(
                uri,
                "application/llsd+xml".into(),
                body,
                token,
                None,
            )
            .await?;
        Ok((200..300).contains(&response.status_code))
    }

    pub async fn request_covenant_notecard_with_uuid_cancellation_token(
        &self,
        covenant_id: UUID,
        ct: Option<CancellationToken>,
    ) -> Result<Option<crate::assets::Asset>, Error> {
        let simulator = match self.current_sim() {
            Ok(simulator) => simulator,
            Err(Error::InvalidOperation) => return Ok(None),
            Err(error) => return Err(error),
        };
        self.request_covenant_notecard_with_uuid_simulator_cancellation_token(
            covenant_id,
            simulator,
            ct,
        )
        .await
    }

    pub async fn request_covenant_notecard_with_uuid_simulator_cancellation_token(
        &self,
        covenant_id: UUID,
        _simulator: crate::Simulator,
        ct: Option<CancellationToken>,
    ) -> Result<Option<crate::assets::Asset>, Error> {
        self.inner
            .client
            .native_assets()?
            .request_asset_with_uuid_asset_type_boolean_source_type_cancellation_token(
                covenant_id,
                AssetType::Notecard,
                true,
                SourceType::SimEstate,
                ct,
            )
            .await
    }

    pub fn send_estate_change_info_dataserver(
        &self,
        estate_name: String,
        sun_hour: f32,
        flags: RegionFlags,
    ) -> Result<(), Error> {
        self.owner_message(
            "estatechangeinfo",
            vec![
                estate_name,
                flags.0.to_string(),
                ((sun_hour * 1024.0) as i32).to_string(),
            ],
        )
    }

    pub async fn send_estate_change_info(
        &self,
        estate_name: String,
        sun_hour: f32,
        flags: RegionFlags,
        ct: Option<CancellationToken>,
    ) -> Result<(), Error> {
        let token = ct.unwrap_or_default();
        if let Some(uri) = self.capability("EstateChangeInfo", &token).await? {
            if !crate::is_offline_fixture_uri(&uri)
                && !self.inner.live_mutations.load(Ordering::Acquire)
            {
                return Err(Error::InvalidOperation);
            }
            let has = |flag: RegionFlags| flags.0 & flag.0 != 0;
            let payload = OSD::Map(HashMap::from([
                ("estate_name".into(), OSD::String(estate_name.clone())),
                (
                    "is_sun_fixed".into(),
                    OSD::Boolean(has(RegionFlags::SUN_FIXED)),
                ),
                (
                    "is_externally_visible".into(),
                    OSD::Boolean(has(RegionFlags::EXTERNALLY_VISIBLE)),
                ),
                (
                    "allow_direct_teleport".into(),
                    OSD::Boolean(has(RegionFlags::ALLOW_DIRECT_TELEPORT)),
                ),
                (
                    "deny_anonymous".into(),
                    OSD::Boolean(has(RegionFlags::DENY_ANONYMOUS)),
                ),
                (
                    "deny_age_unverified".into(),
                    OSD::Boolean(has(RegionFlags::DENY_AGE_UNVERIFIED)),
                ),
                (
                    "block_bots".into(),
                    OSD::Boolean(has(RegionFlags::DENY_BOTS)),
                ),
                (
                    "allow_voice_chat".into(),
                    OSD::Boolean(has(RegionFlags::ALLOW_VOICE)),
                ),
                (
                    "override_public_access".into(),
                    OSD::Boolean(has(RegionFlags::ALLOW_ACCESS_OVERRIDE)),
                ),
                ("invoice".into(), OSD::UUID(UUID::random()?)),
            ]));
            let body = OSDParser::serialize_llsd_xml_bytes(payload)?;
            let (response, _) = self
                .inner
                .client
                .native_http_caps_client()
                .post_with_uri_string_bytes_cancellation_token_i_progress(
                    uri,
                    "application/llsd+xml".into(),
                    body,
                    token,
                    None,
                )
                .await?;
            if (200..300).contains(&response.status_code) {
                return Ok(());
            }
        }
        self.send_estate_change_info_dataserver(estate_name, sun_hour, flags)
    }

    pub async fn set_region_info(
        &self,
        terraform: bool,
        fly: bool,
        fly_over: bool,
        damage: bool,
        resell: bool,
        pushing: bool,
        join: bool,
        agents: f32,
        bonus: f32,
        block_search: bool,
        maturity: EstateToolsRegionMaturity,
    ) -> Result<(), Error> {
        let token = CancellationToken::default();
        if let Some(uri) = self.capability("DispatchRegionInfo", &token).await? {
            if !crate::is_offline_fixture_uri(&uri)
                && !self.inner.live_mutations.load(Ordering::Acquire)
            {
                return Err(Error::InvalidOperation);
            }
            let payload = OSD::Map(HashMap::from([
                ("block_terraform".into(), OSD::Boolean(terraform)),
                ("block_fly".into(), OSD::Boolean(fly)),
                ("block_fly_over".into(), OSD::Boolean(fly_over)),
                ("allow_damage".into(), OSD::Boolean(damage)),
                ("allow_land_resell".into(), OSD::Boolean(resell)),
                ("agent_limit".into(), OSD::Real(f64::from(agents))),
                ("prim_bonus".into(), OSD::Real(f64::from(bonus))),
                (
                    "sim_access".into(),
                    OSD::String((maturity as i32).to_string()),
                ),
                ("restrict_pushobject".into(), OSD::Boolean(pushing)),
                ("allow_parcel_changes".into(), OSD::Boolean(join)),
                ("block_parcel_search".into(), OSD::Boolean(block_search)),
            ]));
            let body = OSDParser::serialize_llsd_xml_bytes(payload)?;
            let (response, _) = self
                .inner
                .client
                .native_http_caps_client()
                .post_with_uri_string_bytes_cancellation_token_i_progress(
                    uri,
                    "application/llsd+xml".into(),
                    body,
                    token,
                    None,
                )
                .await?;
            if (200..300).contains(&response.status_code) {
                return Ok(());
            }
        }
        self.set_region_info_udp(
            terraform, fly, damage, resell, pushing, join, agents, bonus, maturity,
        )
    }

    pub async fn send_sim_console_command(
        &self,
        command: String,
        response_timeout: Option<Duration>,
        ct: Option<CancellationToken>,
    ) -> Result<Option<String>, Error> {
        let token = ct.unwrap_or_default();
        token.throw_if_cancellation_requested()?;
        let console_lock = self.inner.sim_console_lock.lock().fuse();
        let lock_cancelled = token.cancelled().fuse();
        pin_mut!(console_lock, lock_cancelled);
        let _console_guard = select_biased! {
            guard = console_lock => guard,
            () = lock_cancelled => return Err(Error::Cancelled),
        };
        token.throw_if_cancellation_requested()?;
        let (response_sender, response_receiver) = oneshot::channel();
        let response_sender = Arc::new(Mutex::new(Some(response_sender)));
        let sender = Arc::clone(&response_sender);
        let _subscription = self.inner.network.subscribe_caps(
            "SimConsoleResponse".into(),
            crate::CapsEventQueueCallback::from_handler(move |_name, message, _simulator| {
                let body = message.serialize().ok().and_then(|map| {
                    map.get("body")
                        .or_else(|| map.get("Body"))
                        .and_then(|value| value.as_string().ok())
                });
                if let Some(sender) = sender
                    .lock()
                    .unwrap_or_else(std::sync::PoisonError::into_inner)
                    .take()
                {
                    let _ = sender.send(body);
                }
            }),
        );

        // Register the event waiter before POSTing: simulators may enqueue the
        // response immediately. An unconnected client is still allowed to wait
        // so recorded event-queue playback remains fully offline-testable.
        match self.capability("SimConsoleAsync", &token).await {
            Ok(Some(uri)) => {
                if !crate::is_offline_fixture_uri(&uri)
                    && !self.inner.live_mutations.load(Ordering::Acquire)
                {
                    return Err(Error::InvalidOperation);
                }
                let body = OSDParser::serialize_llsd_xml_bytes(OSD::Map(HashMap::from([(
                    "command".into(),
                    OSD::String(command),
                )])))?;
                let (response, _) = self
                    .inner
                    .client
                    .native_http_caps_client()
                    .post_with_uri_string_bytes_cancellation_token_i_progress(
                        uri,
                        "application/llsd+xml".into(),
                        body,
                        token.clone(),
                        None,
                    )
                    .await?;
                if !(200..300).contains(&response.status_code) {
                    return Ok(None);
                }
            }
            Ok(None) | Err(Error::InvalidOperation) => {}
            Err(error) => return Err(error),
        }

        let (timeout_sender, timeout_receiver) = oneshot::channel();
        let timeout = response_timeout.unwrap_or(Duration::from_secs(10));
        std::thread::Builder::new()
            .name("sim-console-timeout".into())
            .spawn(move || {
                std::thread::sleep(timeout);
                let _ = timeout_sender.send(());
            })
            .map_err(|_| Error::InvalidOperation)?;
        let response = response_receiver.fuse();
        let cancelled = token.cancelled().fuse();
        let timeout = timeout_receiver.fuse();
        pin_mut!(response, cancelled, timeout);
        select_biased! {
            value = response => value.map_err(|_| Error::InvalidOperation),
            () = cancelled => Err(Error::Cancelled),
            _ = timeout => Ok(None),
        }
    }

    pub fn upload_terrain(&self, file_data: Vec<u8>, file_name: String) -> Result<UUID, Error> {
        if file_data.is_empty() || file_data.len() > 128 * 1024 * 1024 || file_name.is_empty() {
            return Err(Error::Argument);
        }
        let assets = self.inner.client.native_assets()?;
        let id = assets.native_stage_estate_upload(file_data)?;
        if let Err(error) = self.owner_message("terrain", vec!["upload filename".into(), file_name])
        {
            assets.native_cancel_staged_estate_upload(id);
            return Err(error);
        }
        Ok(id)
    }
}

impl EstateToolsInner {
    fn handle_land_stat_caps(&self, message: crate::messages::linden::LandStatReplyMessage) {
        if self.disposed.load(Ordering::Acquire) || message.report_data_blocks.len() > 100_000 {
            return;
        }
        let tasks = message
            .report_data_blocks
            .into_iter()
            .map(|row| {
                let id = row.task_id;
                (
                    id,
                    EstateTask {
                        position: row.location,
                        score: row.score,
                        mono_score: row.mono_score,
                        task_id: id,
                        task_local_id: row.task_local_id,
                        task_name: row.task_name,
                        owner_name: row.owner_name,
                    },
                )
            })
            .collect();
        let object_count = i32::try_from(message.total_object_count).unwrap_or(i32::MAX);
        if message.report_type == EstateToolsLandStatReportType::TopScripts as u32 {
            self.events.scripts.emit(TopScriptsReplyEventArgs {
                object_count,
                tasks,
            });
        } else if message.report_type == EstateToolsLandStatReportType::TopColliders as u32 {
            self.events.colliders.emit(TopCollidersReplyEventArgs {
                object_count,
                tasks,
            });
        }
    }

    fn handle_packet(&self, event: crate::network_manager::RawPacketReceivedEventArgs) {
        if self.disposed.load(Ordering::Acquire) || event.data.len() > 8 * 1024 * 1024 {
            return;
        }
        let mut position = 0;
        match event.packet_type {
            PacketType::EstateOwnerMessage => {
                if let Ok(packet) = crate::packets::EstateOwnerMessagePacket::new_from_bytes(
                    &event.data,
                    &mut position,
                ) {
                    self.handle_owner_message(packet);
                }
            }
            PacketType::EstateCovenantReply => {
                if let Ok(packet) = crate::packets::EstateCovenantReplyPacket::new_from_bytes(
                    &event.data,
                    &mut position,
                ) {
                    self.events.covenant.emit(EstateCovenantReplyEventArgs {
                        covenant_id: packet.data.covenant_id,
                        timestamp: i64::from(packet.data.covenant_timestamp),
                        estate_name: wire_text(&packet.data.estate_name),
                        estate_owner_id: packet.data.estate_owner_id,
                    });
                }
            }
            PacketType::LandStatReply => {
                if let Ok(packet) =
                    crate::packets::LandStatReplyPacket::new_from_bytes(&event.data, &mut position)
                {
                    let tasks: HashMap<_, _> = packet
                        .report_data
                        .into_iter()
                        .map(|row| {
                            let id = row.task_id;
                            (
                                id,
                                EstateTask {
                                    position: Vector3 {
                                        x: row.location_x,
                                        y: row.location_y,
                                        z: row.location_z,
                                    },
                                    score: row.score,
                                    mono_score: 0.0,
                                    task_id: id,
                                    task_local_id: row.task_local_id,
                                    task_name: wire_text(&row.task_name),
                                    owner_name: wire_text(&row.owner_name),
                                },
                            )
                        })
                        .collect();
                    let count =
                        i32::try_from(packet.request_data.total_object_count).unwrap_or(i32::MAX);
                    if packet.request_data.report_type
                        == EstateToolsLandStatReportType::TopScripts as u32
                    {
                        self.events.scripts.emit(TopScriptsReplyEventArgs {
                            object_count: count,
                            tasks,
                        });
                    } else if packet.request_data.report_type
                        == EstateToolsLandStatReportType::TopColliders as u32
                    {
                        self.events.colliders.emit(TopCollidersReplyEventArgs {
                            object_count: count,
                            tasks,
                        });
                    }
                }
            }
            _ => {}
        }
    }

    fn handle_owner_message(&self, packet: crate::packets::EstateOwnerMessagePacket) {
        let method = wire_text(&packet.method_data.method);
        let params: Vec<_> = packet
            .param_list
            .iter()
            .map(|row| row.parameter.as_slice())
            .collect();
        if method == "estateupdateinfo" && params.len() >= 5 {
            let Some(owner) = parse_uuid(params[1]) else {
                return;
            };
            let Some(estate_id) = parse_u32(params[2]) else {
                return;
            };
            let Some(flags) = parse_u64(params[3]) else {
                return;
            };
            let Some(sun_hour) = parse_i32(params[4]) else {
                return;
            };
            self.events.update.emit(EstateUpdateInfoReplyEventArgs {
                estate_name: wire_text(params[0]),
                estate_owner: owner,
                estate_id,
                flags: RegionFlags(flags),
                sun_hour,
            });
        } else if method == "setaccess" && params.len() > 5 {
            let Some(estate_id) = parse_u32(params[0]) else {
                return;
            };
            let Some(access_type) = parse_u32(params[1]) else {
                return;
            };
            let ids: Vec<_> = params[6..]
                .iter()
                .filter_map(|value| parse_uuid(value))
                .collect();
            match access_type {
                17 => self.events.users.emit(EstateUsersReplyEventArgs {
                    estate_id,
                    count: parse_i32(params[2]).unwrap_or_default(),
                    allowed_users: ids,
                }),
                18 => self.events.groups.emit(EstateGroupsReplyEventArgs {
                    estate_id,
                    count: parse_i32(params[3]).unwrap_or_default(),
                    allowed_groups: ids,
                }),
                20 => self.events.bans.emit(EstateBansReplyEventArgs {
                    estate_id,
                    count: parse_i32(params[4]).unwrap_or_default(),
                    banned: ids,
                }),
                24 => self.events.managers.emit(EstateManagersReplyEventArgs {
                    estate_id,
                    count: parse_i32(params[5]).unwrap_or_default(),
                    managers: ids,
                }),
                _ => {}
            }
        } else if method == "setexperience" && params.len() >= 5 {
            let blocked_count =
                usize::try_from(parse_i32(params[2]).unwrap_or_default()).unwrap_or_default();
            let trusted_count =
                usize::try_from(parse_i32(params[3]).unwrap_or_default()).unwrap_or_default();
            let allowed_count =
                usize::try_from(parse_i32(params[4]).unwrap_or_default()).unwrap_or_default();
            let ids: Vec<_> = params[5..]
                .iter()
                .filter_map(|value| parse_uuid(value))
                .collect();
            let blocked_end = blocked_count.min(ids.len());
            let trusted_end = blocked_end.saturating_add(trusted_count).min(ids.len());
            let allowed_end = trusted_end.saturating_add(allowed_count).min(ids.len());
            self.events
                .experiences
                .emit(EstateExperienceReplyEventArgs {
                    blocked: ids[..blocked_end].to_vec(),
                    trusted: ids[blocked_end..trusted_end].to_vec(),
                    allowed: ids[trusted_end..allowed_end].to_vec(),
                });
        }
    }
}

fn yn(value: bool) -> String {
    if value { "Y" } else { "N" }.into()
}

fn wire_text(value: &[u8]) -> String {
    String::from_utf8_lossy(value.split(|byte| *byte == 0).next().unwrap_or_default()).into_owned()
}
fn parse_u32(value: &[u8]) -> Option<u32> {
    wire_text(value).parse().ok()
}
fn parse_u64(value: &[u8]) -> Option<u64> {
    wire_text(value).parse().ok()
}
fn parse_i32(value: &[u8]) -> Option<i32> {
    wire_text(value).parse().ok()
}
fn parse_uuid(value: &[u8]) -> Option<UUID> {
    UUID::new_with_bytes_int32(value.to_vec(), 0)
        .ok()
        .or_else(|| UUID::new_with_string(wire_text(value)).ok())
}
fn days_to_letters(days: RegionRestartDays) -> String {
    [
        ('S', RegionRestartDays::SUNDAY),
        ('M', RegionRestartDays::MONDAY),
        ('T', RegionRestartDays::TUESDAY),
        ('W', RegionRestartDays::WEDNESDAY),
        ('R', RegionRestartDays::THURSDAY),
        ('F', RegionRestartDays::FRIDAY),
        ('A', RegionRestartDays::SATURDAY),
    ]
    .into_iter()
    .filter_map(|(letter, day)| (days.0 & day.0 != 0).then_some(letter))
    .collect()
}
fn letters_to_days(value: &str) -> RegionRestartDays {
    RegionRestartDays(value.chars().fold(0, |days, letter| {
        days | match letter.to_ascii_uppercase() {
            'S' => RegionRestartDays::SUNDAY.0,
            'M' => RegionRestartDays::MONDAY.0,
            'T' => RegionRestartDays::TUESDAY.0,
            'W' => RegionRestartDays::WEDNESDAY.0,
            'R' => RegionRestartDays::THURSDAY.0,
            'F' => RegionRestartDays::FRIDAY.0,
            'A' => RegionRestartDays::SATURDAY.0,
            _ => 0,
        }
    }))
}
