//! Agent, friends, groups, directory, and statistics commands for native `TestClient`.

#![allow(
    clippy::elidable_lifetime_names,
    clippy::format_push_string,
    clippy::needless_pass_by_value,
    clippy::too_many_lines,
    private_interfaces
)]

use super::{FakeBackend, InventoryFuture as BackendFuture, LiveBackend, lock};
use libremetaverse::packets::{
    GenericMessagePacket, GenericMessagePacketParamListBlock, PacketType,
};
use libremetaverse::types::compat::{CancellationToken, Subscription};
use libremetaverse::types::{UUID, Vector3, Vector3d};
use libremetaverse::{
    DirectoryManagerAgentSearchData as Person, DirectoryManagerClassified as Classified,
    DirectoryManagerDirectoryParcel as Land, DirectoryManagerEventInfo as EventInfo,
    DirectoryManagerEventsSearchData as Event, DirectoryManagerGroupSearchData as GroupResult,
    DirectoryManagerPlacesSearchData as Place, Group, GroupMember, GroupRole,
};
use std::collections::{BTreeMap, HashMap, HashSet};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant, SystemTime};
use tokio::sync::oneshot;

const MAX_RESULTS: usize = 65_535;

#[derive(Clone, Copy)]
pub(super) enum Command {
    Bots,
    CloneProfile,
    SendGeneric,
    Play,
    Touch,
    Who,
    KeyToName,
    SearchClassifieds,
    SearchEvents,
    SearchGroups,
    SearchLand,
    SearchPeople,
    SearchPlaces,
    ShowEvent,
    Friends,
    MapFriend,
    ActivateGroup,
    GroupMembers,
    GroupRoles,
    Groups,
    InviteGroup,
    JoinGroup,
    LeaveGroup,
    Dilation,
    NetStats,
    RegionInfo,
    Stats,
    Uptime,
}

pub(super) fn commands() -> Vec<super::CommandEntry> {
    use super::{CommandCategory as Cat, CommandHandler};
    use Command as C;
    let service = |command| CommandHandler::Services(command);
    vec![
        (
            "bots",
            "List avatars not observed producing viewer effects",
            Cat::Other,
            service(C::Bots),
        ),
        (
            "cloneprofile",
            "Clone another avatar profile. Usage: cloneprofile [target-uuid] --confirm",
            Cat::Other,
            service(C::CloneProfile),
        ),
        (
            "sendgeneric",
            "Send a generic message. Usage: sendgeneric [method] [parameters...] --confirm",
            Cat::Other,
            service(C::SendGeneric),
        ),
        (
            "play",
            "Play an animation. Usage: play [uuid|name|list|show] [--confirm]",
            Cat::Appearance,
            service(C::Play),
        ),
        (
            "touch",
            "Touch an object. Usage: touch [object-uuid] --confirm",
            Cat::Objects,
            service(C::Touch),
        ),
        (
            "who",
            "List all avatars currently observed",
            Cat::Other,
            service(C::Who),
        ),
        (
            "key2name",
            "Resolve a UUID to an avatar or group name. Usage: key2name [uuid]",
            Cat::Search,
            service(C::KeyToName),
        ),
        (
            "searchclassifieds",
            "Search classified ads. Usage: searchclassifieds [text]",
            Cat::Search,
            service(C::SearchClassifieds),
        ),
        (
            "searchevents",
            "Search events. Usage: searchevents [text]",
            Cat::Search,
            service(C::SearchEvents),
        ),
        (
            "searchgroups",
            "Search groups. Usage: searchgroups [text]",
            Cat::Groups,
            service(C::SearchGroups),
        ),
        (
            "searchland",
            "Search land for sale. Usage: searchland [mainland|auction|estate|all] [max-price] [min-size]",
            Cat::Search,
            service(C::SearchLand),
        ),
        (
            "searchpeople",
            "Search avatars. Usage: searchpeople [text]",
            Cat::Search,
            service(C::SearchPeople),
        ),
        (
            "searchplaces",
            "Search places. Usage: searchplaces [text]",
            Cat::Search,
            service(C::SearchPlaces),
        ),
        (
            "showevent",
            "Show event details. Usage: showevent [event-id]",
            Cat::Other,
            service(C::ShowEvent),
        ),
        (
            "friends",
            "List avatar friends",
            Cat::Friends,
            service(C::Friends),
        ),
        (
            "mapfriend",
            "Locate a mappable friend. Usage: mapfriend [uuid]",
            Cat::Friends,
            service(C::MapFriend),
        ),
        (
            "activategroup",
            "Set a group as active. Usage: activategroup [group-name] --confirm",
            Cat::Groups,
            service(C::ActivateGroup),
        ),
        (
            "groupmembers",
            "List group members. Usage: groupmembers [group-name]",
            Cat::Groups,
            service(C::GroupMembers),
        ),
        (
            "grouproles",
            "List group roles. Usage: grouproles [group-name]",
            Cat::Groups,
            service(C::GroupRoles),
        ),
        (
            "groups",
            "List avatar groups",
            Cat::Groups,
            service(C::Groups),
        ),
        (
            "invitegroup",
            "Invite an avatar to a group. Usage: invitegroup [avatar-uuid] [group-uuid] [role-uuid...] --confirm",
            Cat::Groups,
            service(C::InviteGroup),
        ),
        (
            "joingroup",
            "Join a group. Usage: joingroup [group-name] | joingroup uuid [group-uuid] --confirm",
            Cat::Groups,
            service(C::JoinGroup),
        ),
        (
            "leavegroup",
            "Leave a group. Usage: leavegroup [group-name] --confirm",
            Cat::Groups,
            service(C::LeaveGroup),
        ),
        (
            "dilation",
            "Show time dilation for the current simulator",
            Cat::Simulator,
            service(C::Dilation),
        ),
        (
            "netstats",
            "Show packet and capability utilization statistics",
            Cat::Simulator,
            service(C::NetStats),
        ),
        (
            "regioninfo",
            "Show current simulator information",
            Cat::Simulator,
            service(C::RegionInfo),
        ),
        (
            "stats",
            "Show connection and simulator statistics",
            Cat::Simulator,
            service(C::Stats),
        ),
        (
            "uptime",
            "Show login name, start time, and elapsed time",
            Cat::TestClient,
            service(C::Uptime),
        ),
    ]
}

#[derive(Clone, Debug, Default)]
struct AvatarView {
    id: UUID,
    name: String,
    group: String,
    position: Vector3,
    local_id: u32,
}

#[derive(Clone, Debug, Default)]
struct FriendView {
    id: UUID,
    name: String,
    online: bool,
    region_handle: u64,
    position: Vector3,
}

#[derive(Clone, Debug)]
struct GroupView {
    group: Group,
    members: BTreeMap<UUID, GroupMember>,
    roles: BTreeMap<UUID, GroupRole>,
}

#[derive(Clone, Debug, Default)]
struct SimView {
    name: String,
    id: UUID,
    handle: u64,
    size_x: u32,
    size_y: u32,
    access: String,
    flags: String,
    terrain_base: [UUID; 4],
    terrain_detail: [UUID; 4],
    water_height: f32,
    colo: String,
    cpu_ratio: i32,
    cpu_class: i32,
    product_sku: String,
    product_name: String,
    dilation: f32,
    incoming_bps: i32,
    outgoing_bps: i32,
    resent_out: i32,
    resent_in: i32,
    fps: i32,
    physics_fps: f32,
    agent_updates: f32,
    objects: i32,
    scripted_objects: i32,
    frame_time: f32,
    net_time: f32,
    image_time: f32,
    physics_time: f32,
    script_time: f32,
    other_time: f32,
    agents: i32,
    child_agents: i32,
    active_scripts: i32,
}

#[derive(Clone, Debug, Default)]
struct UtilizationView {
    enabled: bool,
    packets: BTreeMap<String, [i64; 4]>,
    capabilities: BTreeMap<String, [i64; 4]>,
}

#[derive(Clone, Debug, Default)]
struct Snapshot {
    avatars: BTreeMap<UUID, AvatarView>,
    effect_sources: HashSet<UUID>,
    friends: BTreeMap<UUID, FriendView>,
    groups: BTreeMap<UUID, GroupView>,
    classifieds: Vec<Classified>,
    events: Vec<Event>,
    group_results: Vec<GroupResult>,
    land: Vec<Land>,
    people: Vec<Person>,
    places: Vec<Place>,
    event_info: BTreeMap<u32, EventInfo>,
    simulators: Vec<SimView>,
    inbox_count: i32,
    utilization: UtilizationView,
    animations: BTreeMap<String, UUID>,
    signaled_animations: BTreeMap<UUID, i32>,
    key_names: BTreeMap<UUID, (bool, String)>,
}

pub(super) struct State {
    policy: super::inventory::MutationPolicy,
    fake: Snapshot,
    created: Instant,
    created_wall: SystemTime,
}

impl State {
    pub(super) fn new(policy: super::inventory::MutationPolicy) -> Self {
        let fake = Snapshot {
            animations: libremetaverse::Animations::to_dictionary()
                .expect("built-in animation catalog is valid")
                .0
                .into_iter()
                .map(|(id, name)| (name, id))
                .collect(),
            ..Snapshot::default()
        };
        Self {
            policy,
            fake,
            created: Instant::now(),
            created_wall: SystemTime::now(),
        }
    }
}

#[derive(Clone, Debug)]
enum Query {
    Key(UUID),
    Classifieds(String),
    Events(String),
    Groups {
        text: String,
        timeout: Duration,
    },
    Land {
        kind: String,
        max_price: i32,
        min_size: i32,
    },
    People(String),
    Places(String),
    EventInfo(u32),
    MapFriend(UUID),
    GroupMembers(UUID),
    GroupRoles(UUID),
    RefreshGroups,
}

#[derive(Clone, Debug)]
enum Mutation {
    CloneProfile(UUID),
    Generic {
        method: String,
        parameters: Vec<String>,
    },
    Play(UUID),
    Touch(UUID),
    ActivateGroup(UUID),
    Invite {
        avatar: UUID,
        group: UUID,
        roles: Vec<UUID>,
    },
    JoinGroup(UUID),
    LeaveGroup(UUID),
}

pub(super) trait Backend {
    fn services_state(&self) -> &Mutex<State>;
    fn services_name(&self) -> String;
    fn services_snapshot<'a>(
        &'a self,
        cancellation: CancellationToken,
    ) -> BackendFuture<'a, Result<Snapshot, String>>;
    fn services_query<'a>(
        &'a self,
        query: Query,
        cancellation: CancellationToken,
    ) -> BackendFuture<'a, Result<Snapshot, String>>;
    fn services_mutate<'a>(
        &'a self,
        mutation: Mutation,
        cancellation: CancellationToken,
    ) -> BackendFuture<'a, Result<String, String>>;
    fn apply_services_fixture(&self, _fields: &[&str]) -> Result<(), &'static str> {
        Err("services fixture requires fake-grid mode")
    }
}

pub(super) async fn execute<B: Backend + ?Sized>(
    backend: &B,
    command: Command,
    args: &[String],
    cancellation: CancellationToken,
) -> String {
    let result = match command {
        Command::Bots => bots(backend, cancellation).await,
        Command::CloneProfile => clone_profile(backend, args, cancellation).await,
        Command::SendGeneric => send_generic(backend, args, cancellation).await,
        Command::Play => play(backend, args, cancellation).await,
        Command::Touch => touch(backend, args, cancellation).await,
        Command::Who => who(backend, cancellation).await,
        Command::KeyToName => key_to_name(backend, args, cancellation).await,
        Command::SearchClassifieds => search_classifieds(backend, args, cancellation).await,
        Command::SearchEvents => search_events(backend, args, cancellation).await,
        Command::SearchGroups => search_groups(backend, args, cancellation).await,
        Command::SearchLand => search_land(backend, args, cancellation).await,
        Command::SearchPeople => search_people(backend, args, cancellation).await,
        Command::SearchPlaces => search_places(backend, args, cancellation).await,
        Command::ShowEvent => show_event(backend, args, cancellation).await,
        Command::Friends => friends(backend, cancellation).await,
        Command::MapFriend => map_friend(backend, args, cancellation).await,
        Command::ActivateGroup => activate_group(backend, args, cancellation).await,
        Command::GroupMembers => group_members(backend, args, cancellation).await,
        Command::GroupRoles => group_roles(backend, args, cancellation).await,
        Command::Groups => groups(backend, cancellation).await,
        Command::InviteGroup => invite_group(backend, args, cancellation).await,
        Command::JoinGroup => join_group(backend, args, cancellation).await,
        Command::LeaveGroup => leave_group(backend, args, cancellation).await,
        Command::Dilation => dilation(backend, cancellation).await,
        Command::NetStats => netstats(backend, cancellation).await,
        Command::RegionInfo => region_info(backend, cancellation).await,
        Command::Stats => stats(backend, cancellation).await,
        Command::Uptime => Ok(uptime(backend)),
    };
    result.unwrap_or_else(|error| error)
}

fn parse_uuid(value: &str, message: &'static str) -> Result<UUID, String> {
    UUID::new_with_string(value.into()).map_err(|_| message.into())
}

fn authorize<'a, B: Backend + ?Sized>(
    backend: &B,
    args: &'a [String],
    spending: bool,
) -> Result<&'a [String], String> {
    let policy = lock(backend.services_state()).policy;
    if !policy.mutations() {
        return Err("Live mutations are disabled; restart with --allow-live-mutations".into());
    }
    if spending && !policy.spending() {
        return Err("Group join blocked; restart with --allow-spending".into());
    }
    if args.last().map(String::as_str) != Some("--confirm") {
        return Err("This command changes live grid state; append --confirm".into());
    }
    Ok(&args[..args.len() - 1])
}

fn avatar_line(avatar: &AvatarView) -> String {
    format!(
        "{} (Group: {}, Location: {:?}, UUID: {} LocalID: {})",
        avatar.name, avatar.group, avatar.position, avatar.id, avatar.local_id
    )
}

async fn who<B: Backend + ?Sized>(
    backend: &B,
    cancellation: CancellationToken,
) -> Result<String, String> {
    let snapshot = backend.services_snapshot(cancellation).await?;
    Ok(snapshot
        .avatars
        .values()
        .map(avatar_line)
        .collect::<Vec<_>>()
        .join("\n"))
}

async fn bots<B: Backend + ?Sized>(
    backend: &B,
    cancellation: CancellationToken,
) -> Result<String, String> {
    let snapshot = backend.services_snapshot(cancellation).await?;
    Ok(snapshot
        .avatars
        .values()
        .filter(|avatar| !snapshot.effect_sources.contains(&avatar.id))
        .map(|avatar| format!("{} Is Probably a bot", avatar_line(avatar)))
        .collect::<Vec<_>>()
        .join("\n"))
}

async fn clone_profile<B: Backend + ?Sized>(
    backend: &B,
    args: &[String],
    cancellation: CancellationToken,
) -> Result<String, String> {
    let args = authorize(backend, args, true)?;
    if args.len() != 1 {
        return Err("Clones another avatars profile as closely as possible. WARNING: This command will destroy your existing profile! Usage: cloneprofile [targetuuid] --confirm".into());
    }
    let target = parse_uuid(&args[0], "Usage: cloneprofile [targetuuid] --confirm")?;
    backend
        .services_mutate(Mutation::CloneProfile(target), cancellation)
        .await?;
    Ok(format!(
        "Synchronized our profile to the profile of {target}"
    ))
}

async fn send_generic<B: Backend + ?Sized>(
    backend: &B,
    args: &[String],
    cancellation: CancellationToken,
) -> Result<String, String> {
    let args = authorize(backend, args, false)?;
    let Some(method) = args.first() else {
        return Err("Usage: sendgeneric method arg1 arg2 ... --confirm".into());
    };
    let parameters = args[1..].to_vec();
    backend
        .services_mutate(
            Mutation::Generic {
                method: method.clone(),
                parameters: parameters.clone(),
            },
            cancellation,
        )
        .await?;
    let suffix = if parameters.is_empty() {
        String::new()
    } else {
        format!(" {}", parameters.join(" "))
    };
    Ok(format!(
        "Sent generic message with method {method}, params{suffix}"
    ))
}

async fn play<B: Backend + ?Sized>(
    backend: &B,
    args: &[String],
    cancellation: CancellationToken,
) -> Result<String, String> {
    const USAGE: &str = "Usage:\n\tplay list - list the built in animations\n\tplay show - show any currently playing animations\n\tplay UUID --confirm - play an animation asset\n\tplay ANIMATION --confirm - play a value returned from \"play list\"\n";
    if args.len() == 1 && args[0].eq_ignore_ascii_case("list") {
        let snapshot = backend.services_snapshot(cancellation).await?;
        return Ok(snapshot
            .animations
            .keys()
            .cloned()
            .collect::<Vec<_>>()
            .join("\n"));
    }
    if args.len() == 1 && args[0].eq_ignore_ascii_case("show") {
        let snapshot = backend.services_snapshot(cancellation).await?;
        return Ok(snapshot
            .signaled_animations
            .iter()
            .map(|(id, sequence)| {
                snapshot
                    .animations
                    .iter()
                    .find_map(|(name, candidate)| (candidate == id).then_some(name.as_str()))
                    .map_or_else(
                        || format!("The {id} Asset Animation is being played, sequence is {sequence}"),
                        |name| format!("The {name} System Animation is being played, sequence is {sequence}"),
                    )
            })
            .collect::<String>());
    }
    let args = authorize(backend, args, false)?;
    if args.len() != 1 {
        return Err(USAGE.into());
    }
    let snapshot = backend.services_snapshot(cancellation.clone()).await?;
    let id = UUID::new_with_string(args[0].clone())
        .ok()
        .or_else(|| {
            snapshot
                .animations
                .get(&args[0].to_ascii_uppercase())
                .copied()
        })
        .ok_or(USAGE)?;
    backend
        .services_mutate(Mutation::Play(id), cancellation)
        .await?;
    Ok(String::new())
}

async fn touch<B: Backend + ?Sized>(
    backend: &B,
    args: &[String],
    cancellation: CancellationToken,
) -> Result<String, String> {
    let args = authorize(backend, args, false)?;
    if args.len() != 1 {
        return Err("Usage: touch [object-uuid] --confirm".into());
    }
    let id = UUID::new_with_string(args[0].clone())
        .map_err(|_| format!("{} is not a valid UUID", args[0]))?;
    backend
        .services_mutate(Mutation::Touch(id), cancellation)
        .await
}

async fn key_to_name<B: Backend + ?Sized>(
    backend: &B,
    args: &[String],
    cancellation: CancellationToken,
) -> Result<String, String> {
    if args.is_empty() {
        return Err("Usage: key2name UUID".into());
    }
    let key = parse_uuid(args[0].trim(), "invalid UUID")
        .map_err(|_| format!("UUID {} appears to be invalid", args[0].trim()))?;
    let snapshot = backend
        .services_query(Query::Key(key), cancellation)
        .await?;
    snapshot.key_names.get(&key).map_or_else(
        || {
            Ok(
                "Timeout waiting for reply, this could mean the Key is not an avatar or a group"
                    .into(),
            )
        },
        |(group, name)| {
            Ok(format!(
                "{}: {name} {key}",
                if *group { "Group" } else { "Avatar" }
            ))
        },
    )
}

fn search_text(args: &[String], usage: &'static str) -> Result<String, String> {
    if args.is_empty() {
        Err(usage.into())
    } else {
        Ok(args.join(" ").trim_end().into())
    }
}

async fn search_classifieds<B: Backend + ?Sized>(
    backend: &B,
    args: &[String],
    cancellation: CancellationToken,
) -> Result<String, String> {
    let text = search_text(args, "Usage: searchclassifieds [search text]")?;
    let snapshot = backend
        .services_query(Query::Classifieds(text.clone()), cancellation)
        .await?;
    let mut output = format!(
        "Your search string '{text}' returned {} classified ads\n",
        snapshot.classifieds.len()
    );
    for ad in snapshot.classifieds {
        output.push_str(&format!("{}\n", ad.to_string()));
    }
    Ok(output)
}

async fn search_events<B: Backend + ?Sized>(
    backend: &B,
    args: &[String],
    cancellation: CancellationToken,
) -> Result<String, String> {
    let text = search_text(args, "Usage: searchevents [search text]")?;
    let snapshot = backend
        .services_query(Query::Events(text.clone()), cancellation)
        .await?;
    let mut output = String::new();
    if snapshot.events.len() == 1 && snapshot.events[0].id == 0 {
        output.push_str("No Results matched your search string\n");
    } else {
        for event in &snapshot.events {
            output.push_str(&format!(
                "Event ID: {} Event Name: {} Event Date: {}\n",
                event.id, event.name, event.date
            ));
        }
    }
    output.push_str(&format!(
        "Your query '{text}' matched {} Events. ",
        snapshot.events.len()
    ));
    Ok(output)
}

async fn search_groups<B: Backend + ?Sized>(
    backend: &B,
    args: &[String],
    cancellation: CancellationToken,
) -> Result<String, String> {
    let text = search_text(args, "Usage: searchgroups [search text]")?;
    let snapshot = backend
        .services_query(
            Query::Groups {
                text: text.clone(),
                timeout: Duration::from_secs(20),
            },
            cancellation,
        )
        .await?;
    let mut output = String::new();
    if snapshot.group_results.is_empty() {
        output.push_str("Didn't find any groups that matched your query :(\n");
    }
    for group in &snapshot.group_results {
        output.push_str(&format!(
            "Group {} ({}) has {} members\n",
            group.group_name, group.group_id, group.members
        ));
    }
    output.push_str(&format!(
        "Your query '{text}' matched {} Groups. ",
        snapshot.group_results.len()
    ));
    Ok(output)
}

fn land_usage() -> String {
    "Usage: searchland [type] [max price] [min size]\n\twhere [type] is one of: mainland, auction, estate, all\n\tif [max price] or [min size] are 0 that parameter will be ignored\n\nexample: \"searchland mainland 0 512\" // shows the lowest priced mainland that is larger than 512/m2\n".into()
}

async fn search_land<B: Backend + ?Sized>(
    backend: &B,
    args: &[String],
    cancellation: CancellationToken,
) -> Result<String, String> {
    if args.len() < 3 {
        return Err(land_usage());
    }
    let raw = args[0].to_ascii_lowercase();
    let kind = if raw.starts_with("au") {
        "auction"
    } else if raw.starts_with('m') {
        "mainland"
    } else if raw.starts_with('e') {
        "estate"
    } else if raw.starts_with("al") {
        "all"
    } else {
        return Err(land_usage());
    }
    .to_owned();
    let max_price = args[1].parse().map_err(|_| land_usage())?;
    let min_size = args[2].parse().map_err(|_| land_usage())?;
    let snapshot = backend
        .services_query(
            Query::Land {
                kind,
                max_price,
                min_size,
            },
            cancellation,
        )
        .await?;
    let mut output = String::new();
    for parcel in &snapshot.land {
        output.push_str(&format!("{}\n", parcel.to_string()));
    }
    output.push_str(&format!("{} results\n", snapshot.land.len()));
    Ok(output)
}

async fn search_people<B: Backend + ?Sized>(
    backend: &B,
    args: &[String],
    cancellation: CancellationToken,
) -> Result<String, String> {
    let text = search_text(args, "Usage: searchpeople [search text]")?;
    let snapshot = backend
        .services_query(Query::People(text.clone()), cancellation)
        .await?;
    let mut output = String::new();
    if snapshot.people.is_empty() {
        output.push_str("Didn't find any people that matched your query :(\n");
    }
    for person in &snapshot.people {
        output.push_str(&format!(
            "{} {} ({})\n",
            person.first_name, person.last_name, person.agent_id
        ));
    }
    output.push_str(&format!(
        "Your query '{text}' matched {} People. ",
        snapshot.people.len()
    ));
    Ok(output)
}

async fn search_places<B: Backend + ?Sized>(
    backend: &B,
    args: &[String],
    cancellation: CancellationToken,
) -> Result<String, String> {
    let text = search_text(args, "Usage: searchplaces [search text]")?;
    let snapshot = backend
        .services_query(Query::Places(text.clone()), cancellation)
        .await?;
    let mut output = format!(
        "Your search string '{text}' returned {} results\n",
        snapshot.places.len()
    );
    for place in snapshot.places {
        output.push_str(&format!("{}\n", place.to_string()));
    }
    Ok(output)
}

async fn show_event<B: Backend + ?Sized>(
    backend: &B,
    args: &[String],
    cancellation: CancellationToken,
) -> Result<String, String> {
    if args.is_empty() {
        return Err("Usage: showevent [eventID] (use searchevents to get ID)".into());
    }
    let id = args[0]
        .parse()
        .map_err(|_| "Usage: showevent [eventID] (use searchevents to get ID)".to_owned())?;
    let snapshot = backend
        .services_query(Query::EventInfo(id), cancellation)
        .await?;
    let event = snapshot
        .event_info
        .get(&id)
        .ok_or_else(|| format!("Sent query for Event {id}"))?;
    let x = event.global_pos.x.rem_euclid(256.0);
    let y = event.global_pos.y.rem_euclid(256.0);
    Ok(format!(
        "Sent query for Event {id}\nsecondlife://{}/{x}/{y}/0\n{}",
        event.sim_name,
        event.to_string()
    ))
}

async fn friends<B: Backend + ?Sized>(
    backend: &B,
    cancellation: CancellationToken,
) -> Result<String, String> {
    let snapshot = backend.services_snapshot(cancellation).await?;
    if snapshot.friends.is_empty() {
        return Ok("No Friends".into());
    }
    let mut output = format!("has {} friends:\n", snapshot.friends.len());
    for friend in snapshot.friends.values() {
        output.push_str(&format!("{}, {}\n", friend.id, friend.name));
    }
    Ok(output)
}

async fn map_friend<B: Backend + ?Sized>(
    backend: &B,
    args: &[String],
    cancellation: CancellationToken,
) -> Result<String, String> {
    if args.len() != 1 {
        return Err("Usage: mapfriend [avatar UUID]".into());
    }
    let id = parse_uuid(&args[0], "Usage: mapfriend [avatar UUID]")?;
    let snapshot = backend
        .services_query(Query::MapFriend(id), cancellation)
        .await?;
    let friend = snapshot
        .friends
        .get(&id)
        .ok_or_else(|| format!("Timeout waiting for reply, Do you have mapping rights on {id}?"))?;
    if !friend.online || friend.region_handle == 0 {
        return Ok(format!("Found Friend {id}, But they appear to be offline"));
    }
    Ok(format!(
        "Found Friend {id} in {} at {}/{}",
        friend.region_handle, friend.position.x, friend.position.y
    ))
}

fn resolve_group(snapshot: &Snapshot, name: &str) -> Option<(UUID, String)> {
    snapshot
        .groups
        .iter()
        .find(|(_, group)| group.group.name.eq_ignore_ascii_case(name))
        .map(|(id, group)| (*id, group.group.name.clone()))
}

async fn activate_group<B: Backend + ?Sized>(
    backend: &B,
    args: &[String],
    cancellation: CancellationToken,
) -> Result<String, String> {
    let args = authorize(backend, args, false)?;
    if args.is_empty() {
        return Err("Set a group as active. Usage: activategroup GroupName --confirm".into());
    }
    let name = args.join(" ").trim().to_owned();
    let snapshot = backend
        .services_query(Query::RefreshGroups, cancellation.clone())
        .await?;
    let Some((id, _)) = resolve_group(&snapshot, &name) else {
        return Ok(format!(
            "{} doesn't seem to be member of the group {name}",
            backend.services_name()
        ));
    };
    backend
        .services_mutate(Mutation::ActivateGroup(id), cancellation)
        .await
}

async fn group_members<B: Backend + ?Sized>(
    backend: &B,
    args: &[String],
    cancellation: CancellationToken,
) -> Result<String, String> {
    if args.is_empty() {
        return Err("Dump group members to console. Usage: groupmembers GroupName".into());
    }
    let name = args.join(" ").trim().to_owned();
    let groups = backend
        .services_query(Query::RefreshGroups, cancellation.clone())
        .await?;
    let Some((id, canonical)) = resolve_group(&groups, &name) else {
        return Ok(format!(
            "{} doesn't seem to be member of the group {name}",
            backend.services_name()
        ));
    };
    let snapshot = backend
        .services_query(Query::GroupMembers(id), cancellation)
        .await?;
    let group = snapshot
        .groups
        .get(&id)
        .ok_or("Group members reply did not contain the requested group")?;
    let mut output = format!("GroupMembers: GroupUUID {id}\nGroupMembers: GroupName {canonical}\n");
    for member in group.members.keys() {
        output.push_str(&format!("GroupMembers: MemberUUID {member}\n"));
    }
    output.push_str(&format!(
        "GroupMembers: MemberCount {}\n{} got group members",
        group.members.len(),
        backend.services_name()
    ));
    Ok(output)
}

async fn group_roles<B: Backend + ?Sized>(
    backend: &B,
    args: &[String],
    cancellation: CancellationToken,
) -> Result<String, String> {
    if args.is_empty() {
        return Err("Dump group roles to console. Usage: grouproles GroupName".into());
    }
    let name = args.join(" ").trim().to_owned();
    let groups = backend
        .services_query(Query::RefreshGroups, cancellation.clone())
        .await?;
    let Some((id, canonical)) = resolve_group(&groups, &name) else {
        return Ok(format!(
            "{} doesn't seem to have any roles in the group {name}",
            backend.services_name()
        ));
    };
    let snapshot = backend
        .services_query(Query::GroupRoles(id), cancellation)
        .await?;
    let group = snapshot
        .groups
        .get(&id)
        .ok_or("Group roles reply did not contain the requested group")?;
    let mut output = format!("GroupRole: GroupUUID {id}\nGroupRole: GroupName {canonical}\n");
    for role in group.roles.values() {
        output.push_str(&format!(
            "GroupRole: Role {} {}|{}\n",
            role.id, role.name, role.title
        ));
    }
    output.push_str(&format!(
        "GroupRole: RoleCount {}\n{} got group roles",
        group.roles.len(),
        backend.services_name()
    ));
    Ok(output)
}

async fn groups<B: Backend + ?Sized>(
    backend: &B,
    cancellation: CancellationToken,
) -> Result<String, String> {
    let snapshot = backend
        .services_query(Query::RefreshGroups, cancellation)
        .await?;
    if snapshot.groups.is_empty() {
        return Ok("No groups".into());
    }
    let mut output = format!("got {} groups:\n", snapshot.groups.len());
    for (id, group) in snapshot.groups {
        output.push_str(&format!("{id}, {}\n", group.group.name));
    }
    Ok(output)
}

async fn invite_group<B: Backend + ?Sized>(
    backend: &B,
    args: &[String],
    cancellation: CancellationToken,
) -> Result<String, String> {
    let args = authorize(backend, args, false)?;
    if args.len() < 2 {
        return Err("invite an avatar into a group. Usage: invitegroup AvatarUUID GroupUUID RoleUUID* --confirm".into());
    }
    let avatar = parse_uuid(&args[0], "parse error avatar UUID")?;
    let group = parse_uuid(&args[1], "parse error group UUID")?;
    let mut roles = if args.len() == 2 {
        vec![UUID::zero()]
    } else {
        Vec::new()
    };
    for role in &args[2..] {
        if let Ok(id) = UUID::new_with_string(role.clone()) {
            roles.push(id);
        }
    }
    backend
        .services_mutate(
            Mutation::Invite {
                avatar,
                group,
                roles,
            },
            cancellation,
        )
        .await?;
    Ok(format!("invited {avatar} to {group}"))
}

async fn join_group<B: Backend + ?Sized>(
    backend: &B,
    args: &[String],
    cancellation: CancellationToken,
) -> Result<String, String> {
    let args = authorize(backend, args, true)?;
    if args.is_empty() {
        return Err(
            "join a group. Usage: joingroup GroupName | joingroup UUID GroupId --confirm".into(),
        );
    }
    let (id, name) = if args[0].eq_ignore_ascii_case("uuid") {
        if args.len() < 2 {
            return Err(
                "join a group. Usage: joingroup GroupName | joingroup UUID GroupId --confirm"
                    .into(),
            );
        }
        (
            parse_uuid(&args[1], "invalid group UUID")
                .map_err(|_| format!("{} doesn't seem a valid UUID", args[1]))?,
            args[1].clone(),
        )
    } else {
        let name = args.join(" ").trim().to_owned();
        let snapshot = backend
            .services_query(
                Query::Groups {
                    text: name.clone(),
                    timeout: Duration::from_mins(1),
                },
                cancellation.clone(),
            )
            .await?;
        let exact: Vec<_> = snapshot
            .group_results
            .iter()
            .filter(|group| group.group_name.eq_ignore_ascii_case(&name))
            .collect();
        if exact.len() != 1 {
            if snapshot.group_results.is_empty() {
                return Ok(format!("Unable to obtain UUID for group {name}"));
            }
            return Ok(format!(
                "Ambiguous name. Found {} groups",
                snapshot.group_results.len()
            ));
        }
        (exact[0].group_id, exact[0].group_name.clone())
    };
    backend
        .services_mutate(Mutation::JoinGroup(id), cancellation)
        .await?;
    Ok(format!("Joined the group {name}"))
}

async fn leave_group<B: Backend + ?Sized>(
    backend: &B,
    args: &[String],
    cancellation: CancellationToken,
) -> Result<String, String> {
    let args = authorize(backend, args, false)?;
    if args.is_empty() {
        return Err("Leave a group. Usage: leavegroup GroupName --confirm".into());
    }
    let name = args.join(" ").trim().to_owned();
    let snapshot = backend
        .services_query(Query::RefreshGroups, cancellation.clone())
        .await?;
    let Some((id, canonical)) = resolve_group(&snapshot, &name) else {
        return Ok(format!(
            "{} doesn't seem to be member of the group {name}",
            backend.services_name()
        ));
    };
    backend
        .services_mutate(Mutation::LeaveGroup(id), cancellation)
        .await?;
    Ok(format!(
        "{} has left the group {canonical}",
        backend.services_name()
    ))
}

async fn dilation<B: Backend + ?Sized>(
    backend: &B,
    cancellation: CancellationToken,
) -> Result<String, String> {
    let snapshot = backend.services_snapshot(cancellation).await?;
    let sim = snapshot
        .simulators
        .first()
        .ok_or("No current simulator available")?;
    Ok(format!("Dilation is {}", sim.dilation))
}

fn format_bytes(bytes: i64) -> String {
    for (divisor, suffix) in [
        (1_i64 << 30, "GB"),
        (1 << 20, "MB"),
        (1 << 10, "KB"),
        (1, "Bytes"),
    ] {
        if bytes > divisor {
            if divisor == 1 {
                return format!("{bytes} {suffix}");
            }
            let hundredths = i128::from(bytes) * 100 / i128::from(divisor);
            let whole = hundredths / 100;
            let fraction = hundredths.abs() % 100;
            let number = if fraction == 0 {
                whole.to_string()
            } else if fraction % 10 == 0 {
                format!("{whole}.{}", fraction / 10)
            } else {
                format!("{whole}.{fraction:02}")
            };
            return format!("{number} {suffix}");
        }
    }
    "0".into()
}

fn utilization_table(title: &str, rows: &BTreeMap<String, [i64; 4]>, total_title: &str) -> String {
    let mut output = format!("{title:<30}|Sent|Recv| TX Bytes | RX Bytes |\n");
    let mut totals = [0_i64; 4];
    for (name, row) in rows {
        output.push_str(&format!(
            "{name:<30}|{:4}|{:4}|{:<10}|{:<10}|\n",
            row[0],
            row[1],
            format_bytes(row[2]),
            format_bytes(row[3])
        ));
        for index in 0..4 {
            totals[index] += row[index];
        }
    }
    output.push_str(&format!(
        "{total_title:>30}|{:4}|{:4}|{:<10}|{:<10}|\n",
        totals[0],
        totals[1],
        format_bytes(totals[2]),
        format_bytes(totals[3])
    ));
    output
}

async fn netstats<B: Backend + ?Sized>(
    backend: &B,
    cancellation: CancellationToken,
) -> Result<String, String> {
    let snapshot = backend.services_snapshot(cancellation).await?;
    if !snapshot.utilization.enabled {
        return Ok(
            "TrackUtilization is not enabled in Settings.Packets, statistics not available".into(),
        );
    }
    Ok(format!(
        "\n{}\n\n{}",
        utilization_table(
            "Message Name",
            &snapshot.utilization.capabilities,
            "Capabilities Totals"
        ),
        utilization_table(
            "Packet Name",
            &snapshot.utilization.packets,
            "Packet Totals"
        )
    ))
}

async fn region_info<B: Backend + ?Sized>(
    backend: &B,
    cancellation: CancellationToken,
) -> Result<String, String> {
    let snapshot = backend.services_snapshot(cancellation).await?;
    let sim = snapshot
        .simulators
        .first()
        .ok_or("No current simulator available")?;
    let x = sim.handle >> 32;
    let y = sim.handle & 0xffff_ffff;
    Ok(format!(
        "{}\nUUID: {}\nHandle: {} (X: {x} Y: {y})\nSize: X: {}, Y: {}\nAccess: {}\nFlags: {}\nTerrainBase0: {}\nTerrainBase1: {}\nTerrainBase2: {}\nTerrainBase3: {}\nTerrainDetail0: {}\nTerrainDetail1: {}\nTerrainDetail2: {}\nTerrainDetail3: {}\nWater Height: {}\nDatacenter:{}\nCPU Ratio:{}\nCPU Class:{}\nRegion SKU/Type:{} {}\n",
        sim.name,
        sim.id,
        sim.handle,
        sim.size_x,
        sim.size_y,
        sim.access,
        sim.flags,
        sim.terrain_base[0],
        sim.terrain_base[1],
        sim.terrain_base[2],
        sim.terrain_base[3],
        sim.terrain_detail[0],
        sim.terrain_detail[1],
        sim.terrain_detail[2],
        sim.terrain_detail[3],
        sim.water_height,
        sim.colo,
        sim.cpu_ratio,
        sim.cpu_class,
        sim.product_sku,
        sim.product_name
    ))
}

async fn stats<B: Backend + ?Sized>(
    backend: &B,
    cancellation: CancellationToken,
) -> Result<String, String> {
    let snapshot = backend.services_snapshot(cancellation).await?;
    let mut output = String::new();
    for sim in &snapshot.simulators {
        output.push_str(&format!(
            "[{}] Dilation: {} InBPS: {} OutBPS: {} ResentOut: {}  ResentIn: {}\n",
            sim.name,
            sim.dilation,
            sim.incoming_bps,
            sim.outgoing_bps,
            sim.resent_out,
            sim.resent_in
        ));
    }
    let sim = snapshot
        .simulators
        .first()
        .ok_or("No current simulator available")?;
    output.push_str(&format!("Packets in the queue: {}FPS : {} PhysicsFPS : {} AgentUpdates : {} Objects : {} Scripted Objects : {}\n", snapshot.inbox_count, sim.fps, sim.physics_fps, sim.agent_updates, sim.objects, sim.scripted_objects));
    output.push_str(&format!("Frame Time : {} Net Time : {} Image Time : {} Physics Time : {} Script Time : {} Other Time : {}\n", sim.frame_time, sim.net_time, sim.image_time, sim.physics_time, sim.script_time, sim.other_time));
    output.push_str(&format!(
        "Agents : {} Child Agents : {} Active Scripts : {}\n",
        sim.agents, sim.child_agents, sim.active_scripts
    ));
    Ok(output)
}

fn uptime<B: Backend + ?Sized>(backend: &B) -> String {
    let state = lock(backend.services_state());
    let since = state
        .created_wall
        .duration_since(SystemTime::UNIX_EPOCH)
        .unwrap_or_default()
        .as_secs();
    format!(
        "I am {}, Up Since: unix:{since} ({:?})",
        backend.services_name(),
        state.created.elapsed()
    )
}

impl Backend for FakeBackend {
    fn services_state(&self) -> &Mutex<State> {
        &self.services_state
    }
    fn services_name(&self) -> String {
        self.name.clone()
    }
    fn services_snapshot<'a>(
        &'a self,
        cancellation: CancellationToken,
    ) -> BackendFuture<'a, Result<Snapshot, String>> {
        Box::pin(async move {
            if cancellation.is_cancellation_requested() {
                Err("Services query cancelled".into())
            } else {
                Ok(lock(&self.services_state).fake.clone())
            }
        })
    }
    fn services_query<'a>(
        &'a self,
        query: Query,
        cancellation: CancellationToken,
    ) -> BackendFuture<'a, Result<Snapshot, String>> {
        Box::pin(async move {
            if cancellation.is_cancellation_requested() {
                return Err("Services query cancelled".into());
            }
            self.record(format!("CALL service-query {}", query_name(&query)));
            let mut snapshot = lock(&self.services_state).fake.clone();
            match query {
                Query::Key(key) => snapshot.key_names.retain(|id, _| *id == key),
                Query::Classifieds(text) => snapshot
                    .classifieds
                    .retain(|item| contains(&item.name, &text)),
                Query::Events(text) => snapshot.events.retain(|item| contains(&item.name, &text)),
                Query::Groups { text, .. } => snapshot
                    .group_results
                    .retain(|item| contains(&item.group_name, &text)),
                Query::Land {
                    kind: _,
                    max_price,
                    min_size,
                } => snapshot.land.retain(|item| {
                    (max_price <= 0 || item.sale_price <= max_price)
                        && (min_size <= 0 || item.actual_area >= min_size)
                }),
                Query::People(text) => snapshot
                    .people
                    .retain(|item| contains(&item.to_string(), &text)),
                Query::Places(text) => snapshot.places.retain(|item| contains(&item.name, &text)),
                Query::EventInfo(id) => snapshot.event_info.retain(|event, _| *event == id),
                Query::MapFriend(id) => snapshot.friends.retain(|friend, _| *friend == id),
                Query::GroupMembers(id) | Query::GroupRoles(id) => {
                    snapshot.groups.retain(|group, _| *group == id);
                }
                Query::RefreshGroups => {}
            }
            Ok(snapshot)
        })
    }
    fn services_mutate<'a>(
        &'a self,
        mutation: Mutation,
        cancellation: CancellationToken,
    ) -> BackendFuture<'a, Result<String, String>> {
        Box::pin(async move {
            if cancellation.is_cancellation_requested() {
                return Err("Services mutation cancelled".into());
            }
            let mut state = lock(&self.services_state);
            let output = match mutation {
                Mutation::CloneProfile(id) => {
                    self.record(format!("CALL profile-clone {id}"));
                    "Profile synchronized".into()
                }
                Mutation::Generic { method, parameters } => {
                    self.record(format!(
                        "CALL generic-message {method} {}",
                        parameters.join(" ")
                    ));
                    "Generic message sent".into()
                }
                Mutation::Play(id) => {
                    state.fake.signaled_animations.insert(id, 1);
                    self.record(format!("CALL animation-start {id} reliable=true"));
                    "Animation started".into()
                }
                Mutation::Touch(id) => {
                    self.record(format!("CALL object-touch {id}"));
                    format!("Touched object {id}")
                }
                Mutation::ActivateGroup(id) => {
                    let name = state
                        .fake
                        .groups
                        .get(&id)
                        .map_or("(unknown)", |group| group.group.name.as_str());
                    self.record(format!("CALL group-activate {id}"));
                    format!("Active group is now {name} (  )")
                }
                Mutation::Invite {
                    avatar,
                    group,
                    roles,
                } => {
                    self.record(format!(
                        "CALL group-invite avatar={avatar} group={group} roles={}",
                        roles
                            .iter()
                            .map(ToString::to_string)
                            .collect::<Vec<_>>()
                            .join(",")
                    ));
                    "Invitation sent".into()
                }
                Mutation::JoinGroup(id) => {
                    let result = state
                        .fake
                        .group_results
                        .iter()
                        .find(|group| group.group_id == id)
                        .cloned();
                    if let Some(result) = result {
                        state.fake.groups.entry(id).or_insert_with(|| GroupView {
                            group: empty_group(id, result.group_name),
                            ..GroupView::default()
                        });
                    }
                    self.record(format!("CALL group-join {id}"));
                    "Group joined".into()
                }
                Mutation::LeaveGroup(id) => {
                    state.fake.groups.remove(&id);
                    self.record(format!("CALL group-leave {id}"));
                    "Group left".into()
                }
            };
            Ok(output)
        })
    }
    fn apply_services_fixture(&self, fields: &[&str]) -> Result<(), &'static str> {
        apply_fixture(self, fields)
    }
}

fn query_name(query: &Query) -> &'static str {
    match query {
        Query::Key(_) => "key-to-name",
        Query::Classifieds(_) => "classifieds",
        Query::Events(_) => "events",
        Query::Groups { .. } => "groups",
        Query::Land { .. } => "land",
        Query::People(_) => "people",
        Query::Places(_) => "places",
        Query::EventInfo(_) => "event-info",
        Query::MapFriend(_) => "map-friend",
        Query::GroupMembers(_) => "group-members",
        Query::GroupRoles(_) => "group-roles",
        Query::RefreshGroups => "refresh-groups",
    }
}

fn contains(value: &str, query: &str) -> bool {
    value
        .to_ascii_lowercase()
        .contains(&query.to_ascii_lowercase())
}

fn empty_group(id: UUID, name: String) -> Group {
    Group {
        accept_notices: false,
        allow_publish: false,
        charter: String::new(),
        contribution: 0,
        founder_id: UUID::zero(),
        group_membership_count: 0,
        group_roles_count: 0,
        id,
        insignia_id: UUID::zero(),
        list_in_profile: false,
        mature_publish: false,
        member_title: String::new(),
        membership_fee: 0,
        money: 0,
        name,
        open_enrollment: false,
        owner_role: UUID::zero(),
        powers: libremetaverse::GroupPowers(0),
        show_in_list: false,
    }
}

fn fixture_uuid(value: &str) -> Result<UUID, &'static str> {
    UUID::new_with_string(value.into()).map_err(|_| "services fixture UUID is invalid")
}
fn fixture_number<T: std::str::FromStr>(value: &str) -> Result<T, &'static str> {
    value
        .parse()
        .map_err(|_| "services fixture number is invalid")
}
fn fixture_bool(value: &str) -> Result<bool, &'static str> {
    match value {
        "true" => Ok(true),
        "false" => Ok(false),
        _ => Err("services fixture boolean is invalid"),
    }
}
fn fixture_vector(x: &str, y: &str, z: &str) -> Result<Vector3, &'static str> {
    Vector3::new_with_single_single_single(
        fixture_number(x)?,
        fixture_number(y)?,
        fixture_number(z)?,
    )
    .map_err(|_| "services fixture vector is invalid")
}

pub(super) fn apply_fake_directive(
    manager: &super::ClientManager,
    fields: &[&str],
) -> Option<Result<(), &'static str>> {
    if !fields
        .first()
        .is_some_and(|name| name.starts_with("service-"))
    {
        return None;
    }
    let client = fields
        .get(1)
        .and_then(|value| UUID::new_with_string((*value).into()).ok());
    let Some(client) = client else {
        return Some(Err("services fixture client UUID is invalid"));
    };
    let Some(client) = manager.clients.get(&client) else {
        return Some(Err("services fixture client is not registered"));
    };
    Some(client.backend.apply_services_fixture(fields))
}

#[allow(clippy::too_many_lines)]
fn apply_fixture(backend: &FakeBackend, fields: &[&str]) -> Result<(), &'static str> {
    let mut state = lock(&backend.services_state);
    let fixture = &mut state.fake;
    match fields {
        ["service-avatar", _, id, name, group, x, y, z, local, effect] => {
            let id = fixture_uuid(id)?;
            fixture.avatars.insert(
                id,
                AvatarView {
                    id,
                    name: (*name).into(),
                    group: (*group).into(),
                    position: fixture_vector(x, y, z)?,
                    local_id: fixture_number(local)?,
                },
            );
            if fixture_bool(effect)? {
                fixture.effect_sources.insert(id);
            }
            fixture.key_names.insert(id, (false, (*name).into()));
        }
        ["service-friend", _, id, name, online, handle, x, y, z] => {
            let id = fixture_uuid(id)?;
            fixture.friends.insert(
                id,
                FriendView {
                    id,
                    name: (*name).into(),
                    online: fixture_bool(online)?,
                    region_handle: fixture_number(handle)?,
                    position: fixture_vector(x, y, z)?,
                },
            );
        }
        ["service-group", _, id, name] => {
            let id = fixture_uuid(id)?;
            fixture.groups.insert(
                id,
                GroupView {
                    group: empty_group(id, (*name).into()),
                    ..GroupView::default()
                },
            );
            fixture.key_names.insert(id, (true, (*name).into()));
        }
        ["service-group-result", _, id, name, members] => fixture.group_results.push(GroupResult {
            group_id: fixture_uuid(id)?,
            group_name: (*name).into(),
            members: fixture_number(members)?,
        }),
        ["service-group-member", _, group, member] => {
            let group = fixture_uuid(group)?;
            let member = fixture_uuid(member)?;
            fixture
                .groups
                .get_mut(&group)
                .ok_or("service-group must precede member")?
                .members
                .insert(
                    member,
                    GroupMember {
                        contribution: 0,
                        id: member,
                        is_owner: false,
                        online_status: String::new(),
                        powers: libremetaverse::GroupPowers(0),
                        title: String::new(),
                    },
                );
        }
        ["service-group-role", _, group, role, name, title] => {
            let group = fixture_uuid(group)?;
            let role = fixture_uuid(role)?;
            fixture
                .groups
                .get_mut(&group)
                .ok_or("service-group must precede role")?
                .roles
                .insert(
                    role,
                    GroupRole {
                        description: String::new(),
                        group_id: group,
                        id: role,
                        members: 0,
                        name: (*name).into(),
                        powers: libremetaverse::GroupPowers(0),
                        title: (*title).into(),
                    },
                );
        }
        ["service-person", _, id, first, last, online] => fixture.people.push(Person {
            agent_id: fixture_uuid(id)?,
            first_name: (*first).into(),
            last_name: (*last).into(),
            online: fixture_bool(online)?,
        }),
        ["service-classified", _, id, name, price] => fixture.classifieds.push(Classified {
            creation_date: SystemTime::UNIX_EPOCH,
            expiration_date: SystemTime::UNIX_EPOCH,
            flags: libremetaverse::DirectoryManagerClassifiedFlags(0),
            id: fixture_uuid(id)?,
            name: (*name).into(),
            price: fixture_number(price)?,
        }),
        ["service-event", _, id, name, date] => fixture.events.push(Event {
            date: (*date).into(),
            flags: libremetaverse::DirectoryManagerEventFlags::PG,
            id: fixture_number(id)?,
            name: (*name).into(),
            owner: UUID::zero(),
            time: 0,
        }),
        ["service-event-info", _, id, name, sim, x, y, z] => {
            let id = fixture_number(id)?;
            fixture.event_info.insert(
                id,
                EventInfo {
                    amount: 0,
                    category: libremetaverse::DirectoryManagerEventCategories::All,
                    cover: 0,
                    creator: UUID::zero(),
                    date: String::new(),
                    date_utc: 0,
                    desc: String::new(),
                    duration: 0,
                    flags: libremetaverse::DirectoryManagerEventFlags::PG,
                    global_pos: Vector3d::new_with_double_double_double(
                        fixture_number(x)?,
                        fixture_number(y)?,
                        fixture_number(z)?,
                    )
                    .map_err(|_| "services fixture vector is invalid")?,
                    id,
                    name: (*name).into(),
                    sim_name: (*sim).into(),
                },
            );
        }
        ["service-land", _, id, name, area, price] => fixture.land.push(Land {
            actual_area: fixture_number(area)?,
            auction: false,
            dwell: 0.0,
            for_sale: true,
            id: fixture_uuid(id)?,
            name: (*name).into(),
            sale_price: fixture_number(price)?,
        }),
        ["service-place", _, name, sim, x, y, z] => fixture.places.push(Place {
            actual_area: 0,
            billable_area: 0,
            desc: String::new(),
            dwell: 0.0,
            flags: libremetaverse::DirectoryManagerPlacesFlags::NotForSale,
            global_x: fixture_number(x)?,
            global_y: fixture_number(y)?,
            global_z: fixture_number(z)?,
            name: (*name).into(),
            owner_id: UUID::zero(),
            price: 0,
            sku: String::new(),
            sim_name: (*sim).into(),
            snapshot_id: UUID::zero(),
        }),
        ["service-sim", _, name, id, handle, dilation] => fixture.simulators.push(SimView {
            name: (*name).into(),
            id: fixture_uuid(id)?,
            handle: fixture_number(handle)?,
            size_x: 256,
            size_y: 256,
            dilation: fixture_number(dilation)?,
            ..SimView::default()
        }),
        ["service-animation", _, name, id, sequence] => {
            let id = fixture_uuid(id)?;
            fixture.animations.insert((*name).into(), id);
            fixture
                .signaled_animations
                .insert(id, fixture_number(sequence)?);
        }
        ["service-utilization", _, enabled] => {
            fixture.utilization.enabled = fixture_bool(enabled)?;
        }
        [
            "service-utilization-row",
            _,
            kind,
            name,
            tx_count,
            rx_count,
            tx_bytes,
            rx_bytes,
        ] => {
            let row = [
                fixture_number(tx_count)?,
                fixture_number(rx_count)?,
                fixture_number(tx_bytes)?,
                fixture_number(rx_bytes)?,
            ];
            match *kind {
                "packet" => {
                    fixture.utilization.packets.insert((*name).into(), row);
                }
                "capability" => {
                    fixture.utilization.capabilities.insert((*name).into(), row);
                }
                _ => return Err("services utilization kind is invalid"),
            }
        }
        _ => return Err("invalid services fake directive"),
    }
    Ok(())
}

impl Backend for LiveBackend {
    fn services_state(&self) -> &Mutex<State> {
        &self.services_state
    }
    fn services_name(&self) -> String {
        self.name.clone()
    }
    fn services_snapshot<'a>(
        &'a self,
        cancellation: CancellationToken,
    ) -> BackendFuture<'a, Result<Snapshot, String>> {
        Box::pin(async move {
            cancellation
                .throw_if_cancellation_requested()
                .map_err(|_| "Services query cancelled")?;
            let (friends, groups, simulators, inbox, track, animations) = {
                let mut client = lock(&self.client);
                let friends = client.friends().friend_list();
                let groups = client.groups().current_groups();
                let simulators = client.network().simulators.snapshot();
                let inbox = client.network().inbox_count();
                let track = client.settings_ref().packets().track_utilization;
                let animations = client
                    .self_()
                    .signaled_animations
                    .read()
                    .unwrap_or_else(std::sync::PoisonError::into_inner)
                    .clone();
                (friends, groups, simulators, inbox, track, animations)
            };
            let mut snapshot = lock(&self.services_state).fake.clone();
            snapshot.friends = friends
                .into_iter()
                .map(|(id, friend)| {
                    (
                        id,
                        FriendView {
                            id,
                            name: friend.name(),
                            online: friend.is_online(),
                            ..FriendView::default()
                        },
                    )
                })
                .collect();
            snapshot.groups = groups
                .into_iter()
                .map(|(id, group)| {
                    (
                        id,
                        GroupView {
                            group,
                            ..GroupView::default()
                        },
                    )
                })
                .collect();
            if track {
                snapshot.utilization.packets.clear();
                for sim in &simulators {
                    snapshot.utilization.packets.insert(
                        format!("UDP {}", sim.name),
                        [
                            sim.stats.get_sent_packets().unwrap_or_default(),
                            sim.stats.get_recv_packets().unwrap_or_default(),
                            sim.stats.get_sent_bytes().unwrap_or_default(),
                            sim.stats.get_recv_bytes().unwrap_or_default(),
                        ],
                    );
                }
            }
            snapshot.simulators = simulators
                .into_iter()
                .map(sim_view)
                .collect::<Result<_, _>>()?;
            if let Some(current) = self.network.current_sim()
                && let Some(index) = snapshot
                    .simulators
                    .iter()
                    .position(|sim| sim.handle == current.handle)
            {
                snapshot.simulators.swap(0, index);
            }
            snapshot.inbox_count = inbox;
            snapshot.utilization.enabled = track;
            snapshot.signaled_animations = animations.into_iter().collect();
            Ok(snapshot)
        })
    }
    fn services_query<'a>(
        &'a self,
        query: Query,
        cancellation: CancellationToken,
    ) -> BackendFuture<'a, Result<Snapshot, String>> {
        Box::pin(async move {
            match query {
                Query::Key(id) => live_key(self, id, cancellation).await,
                Query::Classifieds(text) => {
                    let stream = self
                        .directory
                        .search_classifieds(
                            text,
                            Some(libremetaverse::DirectoryManagerClassifiedCategories::Any),
                            Some(libremetaverse::DirectoryManagerClassifiedQueryFlags(
                                libremetaverse::DirectoryManagerClassifiedQueryFlags::MATURE.0
                                    | libremetaverse::DirectoryManagerClassifiedQueryFlags::PG.0,
                            )),
                            Some(cancellation.clone()),
                        )
                        .map_err(|_| "Could not start classified search")?;
                    let values = collect_stream(
                        stream,
                        cancellation.clone(),
                        Duration::from_secs(20),
                        "Timeout waiting for simulator to respond to query.",
                    )
                    .await?;
                    let mut snapshot = self.services_snapshot(cancellation).await?;
                    snapshot.classifieds = values;
                    Ok(snapshot)
                }
                Query::Events(text) => {
                    let stream = self
                        .directory
                        .search_events(text, None, None, None, Some(cancellation.clone()))
                        .map_err(|_| "Could not start events search")?;
                    let values = collect_stream(
                        stream,
                        cancellation.clone(),
                        Duration::from_secs(20),
                        "Timeout waiting for simulator to respond.",
                    )
                    .await?;
                    let mut snapshot = self.services_snapshot(cancellation).await?;
                    snapshot.events = values;
                    Ok(snapshot)
                }
                Query::Groups { text, timeout } => {
                    let stream = self
                        .directory
                        .search_groups(text, None, Some(cancellation.clone()))
                        .map_err(|_| "Could not start group search")?;
                    let values = collect_stream(
                        stream,
                        cancellation.clone(),
                        timeout,
                        "Timeout waiting for simulator to respond.",
                    )
                    .await?;
                    let mut snapshot = self.services_snapshot(cancellation).await?;
                    snapshot.group_results = values;
                    Ok(snapshot)
                }
                Query::Land {
                    kind,
                    max_price,
                    min_size,
                } => {
                    let type_flags = match kind.as_str() {
                        "auction" => libremetaverse::DirectoryManagerSearchTypeFlags::AUCTION,
                        "mainland" => libremetaverse::DirectoryManagerSearchTypeFlags::MAINLAND,
                        "estate" => libremetaverse::DirectoryManagerSearchTypeFlags::ESTATE,
                        _ => libremetaverse::DirectoryManagerSearchTypeFlags::ANY,
                    };
                    let mut bits = libremetaverse::DirectoryManagerDirFindFlags::SORT_ASC.0
                        | libremetaverse::DirectoryManagerDirFindFlags::PER_METER_SORT.0
                        | libremetaverse::DirectoryManagerDirFindFlags::INCLUDE_ADULT.0
                        | libremetaverse::DirectoryManagerDirFindFlags::INCLUDE_PG.0
                        | libremetaverse::DirectoryManagerDirFindFlags::INCLUDE_MATURE.0;
                    if max_price > 0 {
                        bits |= libremetaverse::DirectoryManagerDirFindFlags::LIMIT_BY_PRICE.0;
                    }
                    if min_size > 0 {
                        bits |= libremetaverse::DirectoryManagerDirFindFlags::LIMIT_BY_AREA.0;
                    }
                    let stream = self
                        .directory
                        .search_land(
                            type_flags,
                            Some(libremetaverse::DirectoryManagerDirFindFlags(bits)),
                            Some(max_price),
                            Some(min_size),
                            Some(cancellation.clone()),
                        )
                        .map_err(|_| "Could not start land search")?;
                    let values = collect_stream(
                        stream,
                        cancellation.clone(),
                        Duration::from_secs(20),
                        "Timeout waiting for simulator to respond.",
                    )
                    .await?;
                    let mut snapshot = self.services_snapshot(cancellation).await?;
                    snapshot.land = values;
                    Ok(snapshot)
                }
                Query::People(text) => {
                    let stream = self
                        .directory
                        .search_people(text, Some(cancellation.clone()))
                        .map_err(|_| "Could not start people search")?;
                    let values = collect_stream(
                        stream,
                        cancellation.clone(),
                        Duration::from_secs(20),
                        "Timeout waiting for simulator to respond.",
                    )
                    .await?;
                    let mut snapshot = self.services_snapshot(cancellation).await?;
                    snapshot.people = values;
                    Ok(snapshot)
                }
                Query::Places(text) => {
                    let flags = libremetaverse::DirectoryManagerDirFindFlags(
                        libremetaverse::DirectoryManagerDirFindFlags::INCLUDE_PG.0
                            | libremetaverse::DirectoryManagerDirFindFlags::INCLUDE_MATURE.0,
                    );
                    let stream = self
                        .directory
                        .search_places(
                            flags,
                            libremetaverse::ParcelCategory::Any,
                            text,
                            None,
                            None,
                            Some(cancellation.clone()),
                        )
                        .map_err(|_| "Could not start places search")?;
                    let values = collect_stream(
                        stream,
                        cancellation.clone(),
                        Duration::from_secs(20),
                        "Timeout waiting for simulator to respond to query.",
                    )
                    .await?;
                    let mut snapshot = self.services_snapshot(cancellation).await?;
                    snapshot.places = values;
                    Ok(snapshot)
                }
                Query::EventInfo(id) => {
                    let value = tokio::select! { result=self.directory.get_event_info(id,Some(cancellation.clone()))=>result.map_err(|_|"Event lookup failed")?.flatten(), ()=tokio::time::sleep(Duration::from_secs(20))=>return Err("Timeout waiting for simulator to respond.".into()), ()=cancellation.cancelled()=>return Err("Event lookup cancelled".into()) };
                    let mut snapshot = self.services_snapshot(cancellation).await?;
                    if let Some(value) = value {
                        snapshot.event_info.insert(id, value);
                    }
                    Ok(snapshot)
                }
                Query::MapFriend(id) => live_map_friend(self, id, cancellation).await,
                Query::GroupMembers(id) => live_group_members(self, id, cancellation).await,
                Query::GroupRoles(id) => live_group_roles(self, id, cancellation).await,
                Query::RefreshGroups => live_groups(self, cancellation).await,
            }
        })
    }
    fn services_mutate<'a>(
        &'a self,
        mutation: Mutation,
        cancellation: CancellationToken,
    ) -> BackendFuture<'a, Result<String, String>> {
        Box::pin(async move {
            cancellation
                .throw_if_cancellation_requested()
                .map_err(|_| "Services mutation cancelled")?;
            match mutation {
                Mutation::CloneProfile(id) => live_clone_profile(self, id, cancellation).await,
                Mutation::Generic { method, parameters } => live_generic(self, method, parameters),
                Mutation::Play(id) => {
                    self.with_agent(|agent| agent.animation_start(id, true))
                        .map_err(|_| "Could not start animation")?;
                    Ok("Animation started".into())
                }
                Mutation::Touch(id) => live_touch(self, id, cancellation).await,
                Mutation::ActivateGroup(id) => live_activate_group(self, id, cancellation).await,
                Mutation::Invite {
                    avatar,
                    group,
                    roles,
                } => {
                    lock(&self.client)
                        .groups()
                        .invite(group, roles, avatar)
                        .map_err(|_| "Group invitation failed")?;
                    Ok("Invitation sent".into())
                }
                Mutation::JoinGroup(id) => live_group_operation(self, id, true, cancellation).await,
                Mutation::LeaveGroup(id) => {
                    live_group_operation(self, id, false, cancellation).await
                }
            }
        })
    }
}

fn sim_view(sim: libremetaverse::Simulator) -> Result<SimView, String> {
    Ok(SimView {
        name: sim.name.clone(),
        id: sim.id,
        handle: sim.handle,
        size_x: sim.size_x,
        size_y: sim.size_y,
        access: format!("{:?}", sim.access),
        flags: format!("{:?}", sim.flags),
        terrain_base: [
            sim.terrain_base0,
            sim.terrain_base1,
            sim.terrain_base2,
            sim.terrain_base3,
        ],
        terrain_detail: [
            sim.terrain_detail0,
            sim.terrain_detail1,
            sim.terrain_detail2,
            sim.terrain_detail3,
        ],
        water_height: sim.water_height,
        colo: sim.colo_location.clone(),
        cpu_ratio: sim.cpu_ratio,
        cpu_class: sim.cpu_class,
        product_sku: sim.product_sku.clone(),
        product_name: sim.product_name.clone(),
        dilation: sim.stats.dilation(),
        incoming_bps: sim
            .stats
            .get_incoming_bps()
            .map_err(|_| "Could not read simulator statistics")?,
        outgoing_bps: sim
            .stats
            .get_outgoing_bps()
            .map_err(|_| "Could not read simulator statistics")?,
        resent_out: sim
            .stats
            .get_resent_packets()
            .map_err(|_| "Could not read simulator statistics")?,
        resent_in: sim
            .stats
            .get_received_resends()
            .map_err(|_| "Could not read simulator statistics")?,
        fps: sim.stats.fps(),
        physics_fps: sim.stats.physics_fps(),
        agent_updates: sim.stats.agent_updates(),
        objects: sim.stats.objects(),
        scripted_objects: sim.stats.scripted_objects(),
        frame_time: sim.stats.frame_time(),
        net_time: sim.stats.net_time(),
        image_time: sim.stats.image_time(),
        physics_time: sim.stats.physics_time(),
        script_time: sim.stats.script_time(),
        other_time: sim.stats.other_time(),
        agents: sim.stats.agents(),
        child_agents: sim.stats.child_agents(),
        active_scripts: sim.stats.active_scripts(),
    })
}

async fn collect_stream<T>(
    stream: libremetaverse::types::compat::IAsyncEnumerable<T>,
    cancellation: CancellationToken,
    timeout: Duration,
    timeout_message: &'static str,
) -> Result<Vec<T>, String> {
    let deadline = tokio::time::Instant::now() + timeout;
    let mut values = Vec::new();
    loop {
        let next = tokio::select! { value=stream.next()=>value, ()=tokio::time::sleep_until(deadline)=>return Err(timeout_message.into()), ()=cancellation.cancelled()=>return Err("Directory search cancelled".into()) };
        let Some(value) = next else { return Ok(values) };
        if values.len() == MAX_RESULTS {
            return Err("Directory result exceeds 65,535 entries".into());
        }
        values.push(value);
    }
}

async fn live_key(
    backend: &LiveBackend,
    id: UUID,
    cancellation: CancellationToken,
) -> Result<Snapshot, String> {
    let (avatars, groups) = {
        let client = lock(&backend.client);
        (client.avatars(), client.groups())
    };
    let (group_sender, group_receiver) = oneshot::channel();
    let group_sender = Arc::new(Mutex::new(Some(group_sender)));
    let callback = Arc::clone(&group_sender);
    let subscription = groups.subscribe_group_profile(Arc::new(move |event| {
        let group = event.group();
        if group.id == id
            && let Some(sender) = lock(&callback).take()
        {
            let _ = sender.send(group.name);
        }
    }));
    groups
        .request_group_profile(id)
        .map_err(|_| "Group lookup request failed")?;
    let deadline = tokio::time::Instant::now() + Duration::from_secs(10);
    let display_lookup = avatars.get_display_names(vec![id], Some(cancellation.clone()));
    tokio::pin!(display_lookup);
    tokio::pin!(group_receiver);
    let (display, group_name) = tokio::select! {
        result=&mut display_lookup=>{
            let display=result.ok().and_then(|(success,values,_)| success.then_some(values).flatten()).and_then(|values|values.into_iter().find(|value|value.id()==id)).map(|value|value.legacy_full_name()).filter(|name|!name.trim().is_empty());
            if display.is_some() {(display,None)} else {
                let group=tokio::select! {value=&mut group_receiver=>value.ok(),()=tokio::time::sleep_until(deadline)=>None,()=cancellation.cancelled()=>return Err("Key lookup cancelled".into())};
                (None,group)
            }
        },
        value=&mut group_receiver=>(None,value.ok()),
        ()=tokio::time::sleep_until(deadline)=>(None,None),
        ()=cancellation.cancelled()=>return Err("Key lookup cancelled".into()),
    };
    drop(subscription);
    let mut snapshot = backend.services_snapshot(cancellation).await?;
    if let Some(name) = display {
        snapshot.key_names.insert(id, (false, name));
    } else if let Some(name) = group_name {
        snapshot.key_names.insert(id, (true, name));
    }
    Ok(snapshot)
}

async fn live_map_friend(
    backend: &LiveBackend,
    id: UUID,
    cancellation: CancellationToken,
) -> Result<Snapshot, String> {
    let friends = lock(&backend.client).friends();
    let (sender, receiver) = oneshot::channel();
    let sender = Arc::new(Mutex::new(Some(sender)));
    let callback = Arc::clone(&sender);
    let subscription = friends.subscribe_friend_found_reply(Arc::new(move |event| {
        if event.agent_id() == id
            && let Some(sender) = lock(&callback).take()
        {
            let _ = sender.send((event.region_handle(), event.location()));
        }
    }));
    friends
        .map_friend(id)
        .map_err(|_| "Friend map request failed")?;
    let found = tokio::select! { value=receiver=>value.ok(), ()=tokio::time::sleep(Duration::from_secs(10))=>None, ()=cancellation.cancelled()=>return Err("Friend map request cancelled".into()) };
    drop(subscription);
    let mut snapshot = backend.services_snapshot(cancellation).await?;
    let (handle, position) = found
        .ok_or_else(|| format!("Timeout waiting for reply, Do you have mapping rights on {id}?"))?;
    let friend = snapshot.friends.entry(id).or_insert_with(|| FriendView {
        id,
        ..FriendView::default()
    });
    friend.online = handle != 0;
    friend.region_handle = handle;
    friend.position = position;
    Ok(snapshot)
}

async fn live_groups(
    backend: &LiveBackend,
    cancellation: CancellationToken,
) -> Result<Snapshot, String> {
    let groups = lock(&backend.client).groups();
    let cached = groups.current_groups();
    if cached.is_empty() {
        let (sender, receiver) = oneshot::channel();
        let sender = Arc::new(Mutex::new(Some(sender)));
        let callback = Arc::clone(&sender);
        let subscription = groups.subscribe_current_groups(Arc::new(move |event| {
            if let Some(sender) = lock(&callback).take() {
                let _ = sender.send(event.groups());
            }
        }));
        groups
            .request_current_groups()
            .map_err(|_| "Could not refresh groups")?;
        tokio::select! {value=receiver=>{value.map_err(|_|"Group cache reply channel closed")?;},()=tokio::time::sleep(Duration::from_secs(30))=>return Err("Groups cache failed.".into()),()=cancellation.cancelled()=>return Err("Group refresh cancelled".into())}
        drop(subscription);
    }
    backend.services_snapshot(cancellation).await
}

async fn live_group_members(
    backend: &LiveBackend,
    id: UUID,
    cancellation: CancellationToken,
) -> Result<Snapshot, String> {
    let groups = lock(&backend.client).groups();
    let (sender, receiver) = oneshot::channel();
    let sender = Arc::new(Mutex::new(Some(sender)));
    let callback = Arc::clone(&sender);
    let request = Arc::new(Mutex::new(UUID::zero()));
    let expected = Arc::clone(&request);
    let subscription = groups.subscribe_group_members_reply(Arc::new(move |event| {
        if event.request_id() == *lock(&expected)
            && let Some(sender) = lock(&callback).take()
        {
            let _ = sender.send(event.members());
        }
    }));
    *lock(&request) = groups
        .request_group_members(id)
        .map_err(|_| "Group members request failed")?;
    let members = tokio::select! {value=receiver=>value.map_err(|_|"Group members reply channel closed")?,()=tokio::time::sleep(Duration::from_secs(30))=>return Err("Timeout waiting for group members".into()),()=cancellation.cancelled()=>return Err("Group members request cancelled".into())};
    drop(subscription);
    let mut snapshot = backend.services_snapshot(cancellation).await?;
    snapshot
        .groups
        .entry(id)
        .or_insert_with(|| GroupView {
            group: empty_group(id, String::new()),
            ..GroupView::default()
        })
        .members = members.into_iter().collect();
    Ok(snapshot)
}

async fn live_group_roles(
    backend: &LiveBackend,
    id: UUID,
    cancellation: CancellationToken,
) -> Result<Snapshot, String> {
    let groups = lock(&backend.client).groups();
    let (sender, receiver) = oneshot::channel();
    let sender = Arc::new(Mutex::new(Some(sender)));
    let callback = Arc::clone(&sender);
    let request = Arc::new(Mutex::new(UUID::zero()));
    let expected = Arc::clone(&request);
    let subscription = groups.subscribe_group_role_data_reply(Arc::new(move |event| {
        if event.request_id() == *lock(&expected)
            && let Some(sender) = lock(&callback).take()
        {
            let _ = sender.send(event.roles());
        }
    }));
    *lock(&request) = groups
        .request_group_roles(id)
        .map_err(|_| "Group roles request failed")?;
    let roles = tokio::select! {value=receiver=>value.map_err(|_|"Group roles reply channel closed")?,()=tokio::time::sleep(Duration::from_secs(30))=>return Err("Timeout waiting for group roles".into()),()=cancellation.cancelled()=>return Err("Group roles request cancelled".into())};
    drop(subscription);
    let mut snapshot = backend.services_snapshot(cancellation).await?;
    snapshot
        .groups
        .entry(id)
        .or_insert_with(|| GroupView {
            group: empty_group(id, String::new()),
            ..GroupView::default()
        })
        .roles = roles.into_iter().collect();
    Ok(snapshot)
}

async fn live_group_operation(
    backend: &LiveBackend,
    id: UUID,
    join: bool,
    cancellation: CancellationToken,
) -> Result<String, String> {
    let groups = lock(&backend.client).groups();
    let (sender, receiver) = oneshot::channel();
    let sender = Arc::new(Mutex::new(Some(sender)));
    let callback = Arc::clone(&sender);
    let subscription = if join {
        groups.subscribe_group_joined_reply(Arc::new(move |event| {
            if event.group_id() == id
                && let Some(sender) = lock(&callback).take()
            {
                let _ = sender.send(event.success());
            }
        }))
    } else {
        groups.subscribe_group_leave_reply(Arc::new(move |event| {
            if event.group_id() == id
                && let Some(sender) = lock(&callback).take()
            {
                let _ = sender.send(event.success());
            }
        }))
    };
    if join {
        groups
            .request_join_group(id)
            .map_err(|_| "Group join request failed")?;
    } else {
        groups
            .leave_group(id)
            .map_err(|_| "Group leave request failed")?;
    }
    let success = tokio::select! {value=receiver=>value.unwrap_or(false),()=tokio::time::sleep(if join {Duration::from_mins(1)} else {Duration::from_secs(30)})=>return Err(if join{"Timeout waiting to join group".into()}else{"Timeout waiting to leave group".into()}),()=cancellation.cancelled()=>return Err("Group operation cancelled".into())};
    drop(subscription);
    if !success {
        return Err(if join {
            "Unable to join the group".into()
        } else {
            "failed to leave the group".into()
        });
    }
    if join {
        groups
            .activate_group(id)
            .map_err(|_| "Joined group but could not activate it")?;
    }
    Ok(if join {
        "Group joined".into()
    } else {
        "Group left".into()
    })
}

async fn live_activate_group(
    backend: &LiveBackend,
    id: UUID,
    cancellation: CancellationToken,
) -> Result<String, String> {
    let (groups, agent) = {
        let client = lock(&backend.client).clone();
        let groups = client.groups();
        let agent = libremetaverse::AgentManager::new(Some(Arc::new(client)))
            .map_err(|_| "Could not acquire the live agent manager")?;
        (groups, agent)
    };
    let (sender, receiver) = oneshot::channel();
    let sender = Arc::new(Mutex::new(Some(sender)));
    let callback = Arc::clone(&sender);
    let subscription = agent.subscribe_agent_data_reply(Arc::new(move |event| {
        if event.active_group_id() == id
            && let Some(sender) = lock(&callback).take()
        {
            let _ = sender.send((event.group_name(), event.group_title()));
        }
    }));
    groups
        .activate_group(id)
        .map_err(|_| "Failed to activate group")?;
    let (name, title) = tokio::select! {
        value=receiver=>value.map_err(|_|"Active-group reply channel closed")?,
        ()=tokio::time::sleep(Duration::from_secs(30))=>return Err("Failed to activate the group".into()),
        ()=cancellation.cancelled()=>return Err("Group activation cancelled".into()),
    };
    drop(subscription);
    Ok(format!("Active group is now {name} ( {title} )"))
}

async fn live_touch(
    backend: &LiveBackend,
    id: UUID,
    cancellation: CancellationToken,
) -> Result<String, String> {
    let (objects, found) = {
        let client = lock(&backend.client);
        let objects = client.objects();
        let found = client
            .network()
            .simulators
            .snapshot()
            .into_iter()
            .find_map(|sim| {
                sim.objects_primitives
                    .read()
                    .unwrap_or_else(std::sync::PoisonError::into_inner)
                    .values()
                    .find(|prim| prim.id == id)
                    .map(|prim| (sim.clone(), prim.local_id))
            });
        (objects, found)
    };
    let (sim, local) = found.ok_or_else(|| format!("Unable to find object {id}"))?;
    objects
        .click_object_with_simulator_u_int32_cancellation_token(sim, local, Some(cancellation))
        .await
        .map_err(|_| "Object touch failed")?;
    Ok(format!("Touched object {local}"))
}

fn live_generic(
    backend: &LiveBackend,
    method: String,
    parameters: Vec<String>,
) -> Result<String, String> {
    let (agent, session, sim) = {
        let mut client = lock(&backend.client);
        let agent = client.self_();
        (
            agent.agent_id(),
            agent.session_id(),
            client.network().current_sim(),
        )
    };
    let sim = sim.ok_or("No current simulator available")?;
    let mut packet = GenericMessagePacket::new_with_constructor()
        .map_err(|_| "Could not construct generic message")?;
    packet.agent_data.agent_id = agent;
    packet.agent_data.session_id = session;
    packet.agent_data.transaction_id = UUID::zero();
    packet.method_data.method = method.into_bytes();
    packet.method_data.invoice = UUID::zero();
    packet.param_list = parameters
        .into_iter()
        .map(|value| {
            let mut block = GenericMessagePacketParamListBlock::new_with_constructor()
                .map_err(|_| "Could not construct generic parameter")?;
            block.parameter = value.into_bytes();
            Ok(block)
        })
        .collect::<Result<_, String>>()?;
    let bytes = packet
        .to_bytes_with_method()
        .map_err(|_| "Could not encode generic message")?;
    sim.send_packet_data(
        bytes.clone(),
        i32::try_from(bytes.len()).map_err(|_| "Generic message is too large")?,
        PacketType::GenericMessage,
        false,
    )
    .map_err(|_| "Could not send generic message")?;
    Ok("Generic message sent".into())
}

async fn live_clone_profile(
    backend: &LiveBackend,
    target: UUID,
    cancellation: CancellationToken,
) -> Result<String, String> {
    let (avatars, own, client, groups) = {
        let mut client = lock(&backend.client);
        let own = client.self_().agent_id();
        (client.avatars(), own, client.clone(), client.groups())
    };
    let agent = libremetaverse::AgentManager::new(Some(Arc::new(client)))
        .map_err(|_| "Could not acquire the live agent manager")?;
    let profile_token = libremetaverse::types::compat::CancellationTokenSource::new();
    let target_profile = tokio::select! {
        result=avatars.request_agent_profile(target,Some(profile_token.token()))=>{let (success,profile)=result.map_err(|_|"Failed to retrieve profile")?;if !success{return Err("Failed to retrieve profile".into())}profile.ok_or("Failed to retrieve profile")?},
        ()=tokio::time::sleep(Duration::from_secs(5))=>{profile_token.cancel();return Err("Failed to retrieve profile".into())},
        ()=cancellation.cancelled()=>{profile_token.cancel();return Err("Profile clone cancelled".into())},
    };
    let interests_token = libremetaverse::types::compat::CancellationTokenSource::new();
    let target_interests = tokio::select! {
        result=avatars.request_avatar_interests(target,Some(interests_token.token()))=>result.map_err(|_|"Failed to retrieve a complete profile for that UUID")?,
        ()=tokio::time::sleep(Duration::from_secs(5))=>{interests_token.cancel();return Err("Failed to retrieve a complete profile for that UUID".into())},
        ()=cancellation.cancelled()=>{interests_token.cancel();return Err("Profile clone cancelled".into())},
    };
    let own_token = libremetaverse::types::compat::CancellationTokenSource::new();
    let own_profile = tokio::select! {
        result=avatars.request_agent_profile(own,Some(own_token.token()))=>{let (success,profile)=result.map_err(|_|"Failed to retrieve current profile picks")?;if !success{return Err("Failed to retrieve current profile picks".into())}profile.ok_or("Failed to retrieve current profile picks")?},
        ()=tokio::time::sleep(Duration::from_secs(5))=>{own_token.cancel();return Err("Failed to retrieve current profile picks".into())},
        ()=cancellation.cancelled()=>{own_token.cancel();return Err("Profile clone cancelled".into())},
    };
    let mut properties = libremetaverse::AvatarAvatarProperties {
        about_text: target_profile.second_life_about_text.clone(),
        born_on: String::new(),
        charter_member: String::new(),
        first_life_image: target_profile.first_life_image_id,
        first_life_text: target_profile.first_life_about_text.clone(),
        flags: libremetaverse::ProfileFlags::ALLOW_PUBLISH,
        partner: UUID::zero(),
        profile_image: target_profile.second_life_image_id,
        profile_url: target_profile.home_page.clone(),
    };
    properties.set_mature_publish(target_profile.is_mature_profile);
    agent
        .update_interests(target_interests)
        .map_err(|_| "Failed to update profile interests")?;
    agent
        .update_profile_with_avatar_properties_cancellation_token(
            properties,
            Some(cancellation.clone()),
        )
        .await
        .map_err(|_| "Failed to update profile")?;
    for pick in own_profile.picks {
        cancellation
            .throw_if_cancellation_requested()
            .map_err(|_| "Profile clone cancelled")?;
        agent
            .pick_delete(pick.id)
            .map_err(|_| "Failed to remove an existing pick")?;
    }
    for pick in target_profile.picks {
        cancellation
            .throw_if_cancellation_requested()
            .map_err(|_| "Profile clone cancelled")?;
        let position = Vector3d::new_with_double_double_double(
            pick.grid_x + pick.region_x,
            pick.grid_y + pick.region_y,
            pick.region_z,
        )
        .map_err(|_| "Target pick contains invalid coordinates")?;
        agent
            .pick_info_update(
                pick.id,
                false,
                pick.parcel_id,
                pick.name,
                position,
                pick.snapshot_id,
                pick.description,
            )
            .map_err(|_| "Failed to copy profile pick")?;
    }
    for group in target_profile.groups {
        cancellation
            .throw_if_cancellation_requested()
            .map_err(|_| "Profile clone cancelled")?;
        if groups.current_groups().contains_key(&group.id) {
            continue;
        }
        live_group_operation(backend, group.id, true, cancellation.clone())
            .await
            .map_err(|error| {
                format!(
                    "Profile updated, but group {} could not be joined: {error}",
                    group.id
                )
            })?;
    }
    Ok("Profile synchronized".into())
}

pub(super) fn install_live(backend: &Arc<LiveBackend>, subscriptions: &mut Vec<Subscription>) {
    let weak = Arc::downgrade(backend);
    let grid = lock(&backend.client).grid();
    let runtime = tokio::runtime::Handle::current();
    subscriptions.push(
        grid.subscribe_coarse_location_update(Arc::new(move |event| {
            let Some(backend) = weak.upgrade() else {
                return;
            };
            let simulator = event.simulator();
            let reverse = simulator
                .global_to_local_id
                .read()
                .unwrap_or_else(std::sync::PoisonError::into_inner)
                .iter()
                .map(|(id, local)| (*id, *local))
                .collect::<HashMap<_, _>>();
            let positions = event.positions();
            let ids = positions.keys().copied().collect::<Vec<_>>();
            {
                let mut state = lock(&backend.services_state);
                for (id, position) in positions {
                    let local = reverse.get(&id).copied().unwrap_or_default();
                    let avatar = state.fake.avatars.entry(id).or_insert_with(|| AvatarView {
                        id,
                        ..AvatarView::default()
                    });
                    avatar.position = position;
                    avatar.local_id = local;
                }
                let removed = event.removed_entries();
                state.fake.avatars.retain(|id, _| !removed.contains(id));
            }
            if ids.is_empty() {
                return;
            }
            let weak = Arc::downgrade(&backend);
            let avatars = lock(&backend.client).avatars();
            runtime.spawn(async move {
                let Ok((true, Some(names), _)) = avatars.get_display_names(ids, None).await else {
                    return;
                };
                let Some(backend) = weak.upgrade() else {
                    return;
                };
                let mut state = lock(&backend.services_state);
                for name in names {
                    if let Some(avatar) = state.fake.avatars.get_mut(&name.id()) {
                        avatar.name = name.legacy_full_name();
                    }
                }
            });
        })),
    );
    let weak = Arc::downgrade(backend);
    subscriptions.push(backend.network.subscribe_packet(
        PacketType::ViewerEffect,
        Arc::new(move |event| {
            let Some(backend) = weak.upgrade() else {
                return;
            };
            let Some(bytes) = event.raw_data() else {
                return;
            };
            let mut index = 0;
            let Ok(packet) = libremetaverse::packets::ViewerEffectPacket::new_with_bytes_int32(
                bytes, &mut index,
            ) else {
                return;
            };
            let mut state = lock(&backend.services_state);
            for effect in packet.effect {
                state.fake.effect_sources.insert(effect.agent_id);
            }
        }),
        false,
    ));
}

impl Default for GroupView {
    fn default() -> Self {
        Self {
            group: empty_group(UUID::zero(), String::new()),
            members: BTreeMap::new(),
            roles: BTreeMap::new(),
        }
    }
}
